File Coverage

blib/lib/App/Critique/Command/collect.pm
Criterion Covered Total %
statement 21 198 10.6
branch 0 68 0.0
condition 0 9 0.0
subroutine 7 23 30.4
pod 2 8 25.0
total 30 306 9.8


line stmt bran cond sub pod time code
1             package App::Critique::Command::collect;
2              
3 1     1   879 use strict;
  1         4  
  1         25  
4 1     1   4 use warnings;
  1         2  
  1         39  
5              
6             our $VERSION = '0.05';
7             our $AUTHORITY = 'cpan:STEVAN';
8              
9 1     1   5 use Path::Tiny ();
  1         2  
  1         13  
10 1     1   477 use Term::ANSIColor ':constants';
  1         5287  
  1         625  
11 1     1   498 use Parallel::ForkManager ();
  1         4391  
  1         19  
12              
13 1     1   7 use App::Critique::Session;
  1         2  
  1         21  
14              
15 1     1   5 use App::Critique -command;
  1         2  
  1         10  
16              
17             sub opt_spec {
18 0     0 1   my ($class) = @_;
19             return (
20 0           [ 'root=s', 'directory to start traversal from (default is root of git work tree)' ],
21             [],
22             [ 'no-violation', 'filter files that contain no Perl::Critic violations ' ],
23             [],
24             [ 'filter|f=s', 'filter files to remove with this regular expression' ],
25             [ 'match|m=s', 'match files to keep with this regular expression' ],
26             [],
27             [ 'n=i', 'number of concurrent processes across which to partition the filtering job', { default => 0 } ],
28             [],
29             [ 'dry-run', 'display list of files, but do not store them' ],
30             [],
31             $class->SUPER::opt_spec,
32             );
33             }
34              
35             our $PAUSE_PROCESSING = 0;
36              
37             sub execute {
38 0     0 1   my ($self, $opt, $args) = @_;
39              
40 0           local $Term::ANSIColor::AUTORESET = 1;
41              
42 0           my $session = $self->cautiously_load_session( $opt, $args );
43              
44 0           info('Session file loaded.');
45              
46 0 0         my $root = $opt->root
47             ? Path::Tiny::path( $opt->root )
48             : $session->git_work_tree;
49              
50 0           my $git_root = $session->git_work_tree_root;
51 0           my $file_predicate = generate_file_predicate(
52             $session => (
53             filter => $opt->filter,
54             match => $opt->match,
55             no_violation => $opt->no_violation
56             )
57             );
58              
59 0           my @all;
60             eval {
61 0           find_all_perl_files(
62             root => $git_root,
63             path => $root,
64             accumulator => \@all,
65             );
66 0           my $unfiltered_count = scalar @all;
67 0           info('Accumulated %d files, now processing', $unfiltered_count);
68              
69 0           filter_files(
70             root => $git_root,
71             files => \@all,
72             filter => $file_predicate,
73             num_procs => $opt->n
74             );
75 0           my $filtered_count = scalar @all;
76 0           info('Filtered %d files, left with %d', $unfiltered_count - $filtered_count, $filtered_count);
77              
78 0           1;
79 0 0         } or do {
80 0           my $e = $@;
81 0           die $e;
82             };
83              
84 0           my $num_files = scalar @all;
85 0           info('Collected %d perl file(s) for critique.', $num_files);
86              
87 0           foreach my $file ( @all ) {
88 0           info(
89             ITALIC('Including %s'),
90             Path::Tiny::path( $file )->relative( $git_root )
91             );
92             }
93              
94 0 0 0       if ( $opt->verbose && $opt->no_violation && $opt->n == 0 ) {
      0        
95 0           my $stats = $session->perl_critic->statistics;
96 0           info(HR_DARK);
97 0           info('STATISTICS(Perl::Critic)');
98 0           info(HR_LIGHT);
99 0           info(BOLD('VIOLATIONS : %s'), format_number($stats->total_violations));
100 0           info('== PERL '.('=' x (TERM_WIDTH() - 8)));
101 0           info(' modules : %s', format_number($stats->modules));
102 0           info(' subs : %s', format_number($stats->subs));
103 0           info(' statements : %s', format_number($stats->statements));
104 0           info('== LINES '.('=' x (TERM_WIDTH() - 9)));
105 0           info(BOLD('TOTAL : %s'), format_number($stats->lines));
106 0           info(' perl : %s', format_number($stats->lines_of_perl));
107 0           info(' pod : %s', format_number($stats->lines_of_pod));
108 0           info(' comments : %s', format_number($stats->lines_of_comment));
109 0           info(' data : %s', format_number($stats->lines_of_data));
110 0           info(' blank : %s', format_number($stats->lines_of_blank));
111 0           info(HR_DARK);
112             }
113              
114 0 0         if ( $opt->dry_run ) {
115 0           info('[dry run] %s file(s) found, 0 files added.', format_number($num_files));
116             }
117             else {
118 0           $session->set_tracked_files( @all );
119 0           $session->reset_file_idx;
120 0           info('Sucessfully added %s file(s).', format_number($num_files));
121              
122 0           $self->cautiously_store_session( $session, $opt, $args );
123 0           info('Session file stored successfully (%s).', $session->session_file_path);
124             }
125             }
126              
127             sub filter_files {
128 0     0 0   my %args = @_;
129 0 0         if ( $args{num_procs} == 0 ) {
130 0           filter_files_serially( %args );
131             }
132             else {
133 0           filter_files_parallel( %args );
134             }
135             }
136              
137             sub filter_files_parallel {
138 0     0 0   my %args = @_;
139 0           my $root = $args{root}; # the reason for `root` is to pass to the filter
140 0           my $all = $args{files};
141 0           my $filter = $args{filter};
142 0           my $num_procs = $args{num_procs};
143              
144 0           my $num_files = scalar( @$all );
145 0           my $temp_dir = Path::Tiny->tempdir;
146              
147 0           my $partition_size = int($num_files / $num_procs);
148 0           my $remainder = int($num_files % $num_procs);
149              
150 0           info('Number of files : %d', $num_files);
151 0           info('Number of processes : %d', $num_procs);
152 0           info('Partition size : %d', $partition_size);
153 0           info('Remainder : %d', $remainder);
154 0           info('Total <%5d> : %d', $num_files, (($partition_size * $num_procs) + $remainder));
155              
156 0           my $pm = Parallel::ForkManager->new(
157             $num_procs,
158             $temp_dir,
159             );
160              
161 0           my @filtered_all;
162             $pm->run_on_finish(
163             sub {
164 0     0     my ($pid, $exit_code, $ident, $exit_signal, $core_dump, $data_structure_reference) = @_;
165 0 0         if ( defined $data_structure_reference ) {
166 0           push @filtered_all => @{ $data_structure_reference };
  0            
167             }
168             else {
169 0           die "Whoa dude, what happened!";
170             }
171             }
172 0           );
173              
174             my @partitions = map {
175 0           [
176 0 0         (($partition_size * $_) - $partition_size) + (($_ == 1) ? 0 : 1),
177             ($partition_size * $_),
178             ]
179             } 1 .. $num_procs;
180              
181             # this will come out to length + 1
182             # so we want to trim off the end
183 0           $partitions[ -1 ]->[ 1 ]--;
184             # then add the remainder here
185 0           $partitions[ -1 ]->[ 1 ] += $remainder;
186              
187             PROCESS_LOOP:
188 0           while ( @partitions ) {
189 0           my ($start, $end) = @{ shift @partitions };
  0            
190              
191             #use Data::Dumper;
192             #warn Dumper [ $start, $end ];
193              
194 0 0         $pm->start and next PROCESS_LOOP;
195              
196 0           my @filtered;
197              
198 0           foreach my $i ( $start .. $end ) {
199 0           my $path = $all->[ $i ];
200              
201 0           info('[%d] Processing file %s', $$, $path);
202 0 0         if ( $filter->( $root, $path ) ) {
203 0           info(BOLD('[%d] Keeping file %s'), $$, $path);
204 0           push @filtered => $path;
205             }
206             }
207              
208 0           $pm->finish(0, \@filtered);
209             }
210              
211 0           $pm->wait_all_children;
212              
213 0           @$all = @filtered_all;
214             }
215              
216             sub filter_files_serially {
217 0     0 0   my %args = @_;
218 0           my $root = $args{root}; # the reason for `root` is to pass to the filter
219 0           my $all = $args{files};
220 0           my $filter = $args{filter};
221              
222 0     0     local $SIG{INT} = sub { $PAUSE_PROCESSING++ };
  0            
223              
224 0           my $num_processed = 0;
225              
226 0           my @filtered_all;
227 0           while ( @$all ) {
228 0 0         if ( $PAUSE_PROCESSING ) {
229 0           warning('[processing paused]');
230              
231             PROMPT:
232             my $continue = prompt_str(
233             '>> (r)esume (h)alt (a)bort | (s)tatus ',
234             {
235 0     0     valid => sub { $_[0] =~ m/[rhas]{1}/ },
236 0           default => 'r',
237             }
238             );
239              
240 0 0         if ( $continue eq 'r' ) {
    0          
    0          
    0          
241 0           warning('[resuming]');
242 0           $PAUSE_PROCESSING = 0;
243             }
244             elsif ( $continue eq 'h' ) {
245 0           warning('[halt processing - retaining results accumulated so far]');
246 0           last;
247             }
248             elsif ( $continue eq 'a' ) {
249 0           warning('[abort processing - discarding all results]');
250 0           @filtered_all = ();
251 0           last;
252             }
253             elsif ( $continue eq 's' ) {
254 0           warning( join "\n" => @filtered_all );
255 0           warning('[Processed %d files so far]', $num_processed );
256 0           warning('[Accumulated %d files so far]', scalar @filtered_all );
257 0           goto PROMPT;
258             }
259             }
260              
261 0           my $path = shift @$all;
262              
263 0           info('Processing file %s', $path);
264 0 0         if ( $filter->( $root, $path ) ) {
265 0           info(BOLD('Keeping file %s'), $path);
266 0           push @filtered_all => $path;
267             }
268              
269 0           $num_processed++;
270             }
271              
272 0           @$all = @filtered_all;
273             }
274              
275             sub find_all_perl_files {
276 0     0 0   my %args = @_;
277 0           my $root = $args{root}; # the reason for `root` is to have nicer output (just FYI)
278 0           my $path = $args{path};
279 0           my $acc = $args{accumulator};
280              
281 0 0         if ( $path->is_file ) {
    0          
282             # ignore anything but perl files ...
283 0 0         return unless is_perl_file( $path->stringify );
284              
285 0           info('... adding file (%s)', $path->relative( $root )); # this should be the only usafe of root
286 0           push @$acc => $path;
287             }
288             elsif ( -l $path ) { # Path::Tiny does not have a test for symlinks
289             ;
290             }
291             else {
292 0           my @children = $path->children( qr/^[^.]/ );
293              
294             # prune the directories we really don't care about
295 0 0         if ( my $ignore = $App::Critique::CONFIG{'IGNORE'} ) {
296 0           @children = grep !$ignore->{ $_->basename }, @children;
297             }
298              
299             # recurse ...
300 0           foreach my $child ( @children ) {
301 0           find_all_perl_files(
302             root => $root,
303             path => $child,
304             accumulator => $acc,
305             );
306             }
307             }
308              
309 0           return;
310             }
311              
312             sub generate_file_predicate {
313 0     0 0   my ($session, %args) = @_;
314              
315 0           my $filter = $args{filter};
316 0           my $match = $args{match};
317 0           my $no_violation = $args{no_violation};
318              
319 0           my $c = $session->perl_critic;
320              
321             # lets build an array of code_ref filters, that will be use to filter
322             # the files, the code refs assume the params will be $path,$rel.
323              
324             #-------------------------------#
325             # match | filter | no-violation #
326             #-------------------------------#
327             # 1 | 1 | 1 # collect with match, filter and no violations
328             # 1 | 1 | 0 # collect with match and filter
329             # 1 | 0 | 1 # collect with match and no violations
330             # 1 | 0 | 0 # collect with match
331             #-------------------------------#
332             # 0 | 1 | 1 # collect with filter and no violations
333             # 0 | 1 | 0 # collect with filter
334             #-------------------------------#
335             # 0 | 0 | 1 # collect with no violations
336             #-------------------------------#
337             # 0 | 0 | 0 # collect
338             #-------------------------------#
339              
340 0     0     my @filters = (sub { return 1 });
  0            
341 0 0   0     push @filters, sub { return $_[1] =~ /$match/ } if $match ;
  0            
342 0 0   0     push @filters, sub { return $_[1] !~ /$filter/} if $filter;
  0            
343             push @filters, sub {
344 0     0     return scalar $c->critique( $_[0]->stringify )
345 0 0         } if $no_violation;
346              
347             my $predicate = sub {
348 0     0     my ($root,$path) = @_;
349 0           my $rel = $path->relative( $root );
350 0           for my $file_filter( @filters ) {
351 0 0         return unless $file_filter->($path,$rel);
352             }
353 0           return 1;
354 0           };
355              
356 0           $session->set_file_criteria({
357             filter => $filter,
358             match => $match,
359             no_violation => $no_violation
360             });
361              
362 0           return $predicate;
363             }
364              
365             # NOTE:
366             # This was mostly taken from the guts of
367             # Perl::Critic::Util::{_is_perl,_is_backup}
368             # - SL
369             sub is_perl_file {
370 0     0 0   my ($file) = @_;
371              
372             # skip all the backups
373 0 0         return 0 if $file =~ m{ [.] swp \z}xms;
374 0 0         return 0 if $file =~ m{ [.] bak \z}xms;
375 0 0         return 0 if $file =~ m{ ~ \z}xms;
376 0 0         return 0 if $file =~ m{ \A [#] .+ [#] \z}xms;
377              
378             # but grab the perl files
379 0 0         return 1 if $file =~ m{ [.] PL \z}xms;
380 0 0         return 1 if $file =~ m{ [.] p[lm] \z}xms;
381 0 0         return 1 if $file =~ m{ [.] t \z}xms;
382 0 0         return 1 if $file =~ m{ [.] psgi \z}xms;
383 0 0         return 1 if $file =~ m{ [.] cgi \z}xms;
384              
385             # if we have to, check for shebang
386 0           my $first;
387             {
388 0 0         open my $fh, '<', $file or return 0;
  0            
389 0           $first = <$fh>;
390 0           close $fh;
391             }
392              
393 0 0 0       return 1 if defined $first && ( $first =~ m{ \A [#]!.*perl }xms );
394 0           return 0;
395             }
396              
397             1;
398              
399             =pod
400              
401             =head1 NAME
402              
403             App::Critique::Command::collect - Collect set of files for current critique session
404              
405             =head1 VERSION
406              
407             version 0.05
408              
409             =head1 DESCRIPTION
410              
411             This command will traverse the critque directory and gather all available Perl
412             files for critiquing. It will then store this list inside the correct critique
413             session file for processing during the critique session.
414              
415             It should be noted that this is a destructive command, meaning that if you have
416             begun critiquing your files and you re-run this command it will overwrite that
417             list and you will loose any tracking information you currently have.
418              
419             =head1 AUTHOR
420              
421             Stevan Little <stevan@cpan.org>
422              
423             =head1 COPYRIGHT AND LICENSE
424              
425             This software is copyright (c) 2016 by Stevan Little.
426              
427             This is free software; you can redistribute it and/or modify it under
428             the same terms as the Perl 5 programming language system itself.
429              
430             =cut
431              
432             __END__
433              
434             # ABSTRACT: Collect set of files for current critique session
435