File Coverage

blib/lib/App/Critique/Command/process.pm
Criterion Covered Total %
statement 21 196 10.7
branch 0 62 0.0
condition 0 9 0.0
subroutine 7 15 46.6
pod 2 6 33.3
total 30 288 10.4


line stmt bran cond sub pod time code
1             package App::Critique::Command::process;
2              
3 1     1   927 use strict;
  1         3  
  1         26  
4 1     1   5 use warnings;
  1         2  
  1         39  
5              
6             our $VERSION = '0.05';
7             our $AUTHORITY = 'cpan:STEVAN';
8              
9 1     1   6 use Path::Tiny ();
  1         2  
  1         14  
10 1     1   5 use List::Util ();
  1         2  
  1         12  
11 1     1   21 use Term::ANSIColor ':constants';
  1         3  
  1         212  
12              
13 1     1   7 use App::Critique::Session;
  1         2  
  1         19  
14              
15 1     1   4 use App::Critique -command;
  1         2  
  1         6  
16              
17             sub opt_spec {
18 0     0 1   my ($class) = @_;
19             return (
20 0           [ 'reset', 'resets the file index to 0', { default => 0 } ],
21             [ 'back', 'back up and re-process the last file', { default => 0 } ],
22             [ 'next', 'skip over processing the current file', { default => 0 } ],
23             [ 'goto=i', 'goto to file at given index' ],
24             [],
25             [ 'blame', 'show the `git blame` block for each violation', { default => 0 } ],
26             [],
27             $class->SUPER::opt_spec
28             );
29             }
30              
31             sub execute {
32 0     0 1   my ($self, $opt, $args) = @_;
33              
34             error('No acceptable value found for EDITOR in the critique config, please set one.')
35 0 0         unless $App::Critique::CONFIG{EDITOR};
36              
37 0           local $Term::ANSIColor::AUTORESET = 1;
38              
39 0           my $session = $self->cautiously_load_session( $opt, $args );
40              
41 0           info('Session file loaded.');
42              
43             # TODO:
44             # check to see if there are changes in the
45             # working directory, if so, exit with an
46             # error.
47              
48             # TODO:
49             # add a new flag that will look at the last
50             # commit and assuming it included the current
51             # file, add the SHA to the session data (with a
52             # special note saying it was commited manually)
53              
54 0           my @tracked_files = $session->tracked_files;
55              
56             # TODO:
57             # not all these options can be given together, so
58             # we should do some validation for that.
59             # - SL
60              
61 0 0         if ( $opt->back ) {
62 0           $session->dec_file_idx;
63 0           $tracked_files[ $session->current_file_idx ]->forget_all;
64             }
65              
66 0 0         if ( $opt->next ) {
67 0           $session->inc_file_idx;
68             }
69              
70 0 0         if ( $opt->reset ) {
71 0           $session->reset_file_idx;
72 0           $_->forget_all foreach @tracked_files;
73             }
74              
75 0 0         if ( my $idx = $opt->goto ) {
76 0           $session->set_file_idx( $idx );
77             }
78              
79 0 0         if ( $session->current_file_idx == scalar @tracked_files ) {
80 0           info(HR_DARK);
81 0           info('All files have already been processed.');
82 0           info(HR_LIGHT);
83 0           info('- run `critique status` to see more information');
84 0           info('- run `critique process --reset` to review all files again');
85 0           info(HR_DARK);
86 0           return;
87             }
88              
89 0           my ($idx, $file);
90              
91             MAIN:
92 0           while (1) {
93              
94 0           info(HR_DARK);
95              
96 0           $idx = $session->current_file_idx;
97 0           $file = $tracked_files[ $idx ];
98              
99 0           my $path = $file->relative_path( $session->git_work_tree_root );
100              
101 0           info('Running Perl::Critic against (%s)', $path);
102 0           info(HR_LIGHT);
103              
104             # perlcritic can fail, so lets guard against it and let the user
105             # decide if they want to carry on
106 0           my @violations;
107             eval {
108 0           @violations = $self->discover_violations( $session, $file, $opt );
109 0           1;
110 0 0         } or do {
111 0           info(HR_ERROR);
112 0           warn($@);
113 0           info(HR_LIGHT);
114 0           my $should_review = prompt_yn(
115             BOLD(sprintf 'A error has occurred do you want to continue?'),
116             { default => 'y' }
117             );
118 0 0         unless ( $should_review ) { exit }
  0            
119 0           next;
120             };
121              
122             # remember it the first time we use it
123             # but do not update it for each re-process
124             # which we do after each edit
125 0 0         $file->remember('violations' => scalar @violations)
126             unless $file->recall('violations');
127              
128 0 0         if ( @violations == 0 ) {
129 0           info(ITALIC('No violations found, proceeding to next file.'));
130 0           next MAIN;
131             }
132             else {
133 0           my $should_review = prompt_yn(
134             BOLD(sprintf 'Found %d violations, would you like to review them?', (scalar @violations)),
135             { default => 'y' }
136             );
137              
138 0 0         if ( $should_review ) {
139              
140 0   0       my ($reviewed, $edited) = (
      0        
141             $file->recall('reviewed') // 0,
142             $file->recall('edited') // 0,
143             );
144              
145 0           foreach my $violation ( @violations ) {
146              
147 0           $self->display_violation( $session, $file, $violation, $opt );
148 0           $reviewed++;
149              
150 0           my $should_edit = prompt_yn(
151             BOLD('Would you like to fix this violation?'),
152             { default => 'y' }
153             );
154              
155 0           my $did_commit = 0;
156              
157 0 0         if ( $should_edit ) {
158 0           $did_commit = $self->edit_violation( $session, $file, $violation );
159 0 0         $edited++ if $did_commit;
160             }
161              
162             # keep state on disc ...
163 0           $file->remember('reviewed', $reviewed);
164 0           $file->remember('edited', $edited);
165 0           $self->cautiously_store_session( $session, $opt, $args );
166              
167 0 0         if ( $did_commit ) {
168 0           info(HR_LIGHT);
169 0           info('File was edited, re-processing is required');
170 0           redo MAIN;
171             }
172             }
173             }
174             }
175              
176             } continue {
177              
178 0           $session->inc_file_idx;
179 0           $self->cautiously_store_session( $session, $opt, $args );
180              
181 0 0         if ( ($idx + 1) == scalar @tracked_files ) {
182 0           info(HR_LIGHT);
183 0           info('Processing complete, run `status` to see results.');
184 0           last MAIN;
185             }
186              
187             }
188              
189             }
190              
191             sub discover_violations {
192 0     0 0   my ($self, $session, $file, $opt) = @_;
193              
194 0           my @violations = $session->perl_critic->critique( $file->path->stringify );
195              
196 0           return @violations;
197             }
198              
199              
200             sub display_violation {
201 0     0 0   my ($self, $session, $file, $violation, $opt) = @_;
202              
203 0           my $rel_filename = Path::Tiny::path( $violation->filename )->relative( $session->git_work_tree_root );
204              
205 0           info(HR_DARK);
206 0           info(BOLD('Violation: %s'), $violation->description);
207 0           info(HR_DARK);
208 0           info('%s', $violation->explanation);
209             #if ( $opt->verbose ) {
210             # info(HR_LIGHT);
211             # info('%s', $violation->diagnostics);
212             #}
213 0           info(HR_LIGHT);
214 0           info(' policy : %s' => $violation->policy);
215 0           info(' severity : %d' => $violation->severity);
216 0           info(' location : %s @ <%d:%d>' => (
217             $rel_filename,
218             $violation->line_number,
219             $violation->column_number
220             ));
221 0           info(HR_LIGHT);
222 0           info(ITALIC('%s'), $violation->source);
223              
224 0 0         if ( $opt->blame ) {
225 0           info(HR_DARK);
226 0           info('%s', $self->blame_violation(
227             $session,
228             $rel_filename,
229             $violation->line_number
230             ));
231             }
232              
233 0           info(HR_LIGHT);
234             }
235              
236             sub blame_violation {
237 0     0 0   my ($self, $session, $rel_filename, $line_num) = @_;
238              
239 0           my $line_count = scalar Path::Tiny::path($rel_filename)->lines;
240 0           my $start_line = $line_num - 5;
241 0           my $end_line = $line_num + 5;
242 0 0         $end_line = $line_count if $end_line > $line_count;
243              
244 0           my @lines = $session->git_wrapper->blame(
245             $rel_filename, {
246             L => (join ',' => $start_line, $end_line )
247             }
248             );
249 0           $lines[5] = BOLD($lines[5]);
250 0           return join "\n" => @lines;
251             }
252              
253             sub edit_violation {
254 0     0 0   my ($self, $session, $file, $violation) = @_;
255              
256 0           my $git = $session->git_wrapper;
257 0           my $rel_filename = $violation->filename;
258 0           my $abs_filename = Path::Tiny::path( $violation->filename )->relative( $session->git_work_tree_root );
259 0           my $policy = $violation->policy;
260 0 0         my $rewriter = $policy->can('rewriter') ? $policy->rewriter( $violation ) : undef;
261              
262 0           my $cmd_fmt = $App::Critique::CONFIG{EDITOR};
263 0           my @cmd_args = (
264             $rel_filename,
265             $violation->line_number,
266             $violation->column_number
267             );
268              
269 0           my $cmd = sprintf $cmd_fmt => @cmd_args;
270              
271             EDIT:
272 0 0 0       if ( $rewriter && $rewriter->can_rewrite ) {
273 0           info(HR_LIGHT);
274 0           info('... attempting to re-write violation.');
275 0           my $document;
276             eval {
277 0           $document = $rewriter->rewrite;
278 0           1;
279 0 0         } or do {
280 0           error('Unable to re-write violation(%s) because (%s)', $violation, $@);
281             };
282 0           info(BOLD('Violation re-written successfully!'));
283 0           info('... attempting to save file(%s)', $abs_filename);
284             eval {
285 0           $document->save( $abs_filename );
286 0           1;
287 0 0         } or do {
288 0           error('Unable to save file(%s) because (%s)', $abs_filename, $@);
289             };
290 0           info(BOLD('File(%s) saved successfully!'), $abs_filename);
291             }
292             else {
293 0           system $cmd;
294             }
295              
296 0           my $statuses = $git->status;
297 0           my @changed = $statuses->get('changed');
298 0           my $did_edit = scalar grep { my $from = $_->from; $abs_filename =~ /$from/ } @changed;
  0            
  0            
299              
300 0 0         if ( $did_edit ) {
301 0           info(HR_DARK);
302 0           info('Changes detected, generating diff.');
303 0           info(HR_LIGHT);
304 0           info('%s', join "\n" => $git->RUN('diff', { 'color' => $App::Critique::CONFIG{COLOR} }));
305 0           my $policy_name = $violation->policy;
306 0           $policy_name =~ s/^Perl\:\:Critic\:\:Policy\:\://;
307              
308 0           my $commit_msg = sprintf "%s - critique(%s)" => $violation->description, $policy_name;
309              
310 0           CHOOSE:
311              
312             info(HR_LIGHT);
313 0           my $commit_this_change = prompt_str(
314             (
315             BOLD('Commit Message:').
316             "\n\n ".(join "\n " => split /\n/ => $commit_msg)."\n\n".
317             BOLD("Press ENTER to accept this message, enter text to be appended to the commit message, or (n)o for more options.\n")
318             ),
319             );
320              
321 0 0         if ( !$commit_this_change ) {
    0          
322 0           info(HR_DARK);
323 0           info('Adding and commiting file (%s) to git', $abs_filename);
324 0           info(HR_LIGHT);
325 0           info('%s', join "\n" => $git->add($rel_filename, { v => 1 }));
326 0           info('%s', join "\n" => $git->commit({ v => 1, message => $commit_msg }));
327              
328 0           my ($sha) = $git->rev_parse('HEAD');
329              
330 0 0         $file->remember('shas' => [ @{ $file->recall('shas') || [] }, $sha ]);
  0            
331 0   0       $file->remember('commited' => ($file->recall('commited') || 0) + 1);
332              
333 0           return 1;
334             }
335             elsif ( lc($commit_this_change) eq 'n' ) {
336 0           info(HR_LIGHT);
337             my $what_now = prompt_str(
338             BOLD('What would you like to do? edit the (f)ile, edit the (c)ommit message or (a)ppend the commit message'),
339 0     0     { valid => sub { $_[0] =~ m/[fca]{1}/ } }
340 0           );
341              
342 0 0         if ( $what_now eq 'c' ) {
    0          
    0          
343 0           info(HR_LIGHT);
344 0           $commit_msg = prompt_str( BOLD('Please write a commit message') );
345 0           $commit_msg =~ s/\\n/\n/g; # un-escape any newlines ...
346 0           goto CHOOSE;
347             }
348             elsif ( $what_now eq 'a' ) {
349 0           info(HR_LIGHT);
350 0           $commit_msg .= "\n\n" . prompt_str( BOLD('Please append the commit message') );
351 0           goto CHOOSE;
352             }
353             elsif ( $what_now eq 'f' ) {
354 0           goto EDIT;
355             }
356             }
357             else {
358 0           $commit_msg .= "\n\n" . $commit_this_change;
359 0           goto CHOOSE;
360             }
361             }
362             else {
363 0           RETRY:
364             info(HR_LIGHT);
365             my $what_now = prompt_str(
366             BOLD('No edits found, would like to (e)dit again, (s)kip this violation or (b)lame the file?'),
367 0     0     { valid => sub { $_[0] =~ m/[esb]{1}/ } }
368 0           );
369              
370 0 0         if ( $what_now eq 'e' ) {
    0          
    0          
371 0           goto EDIT;
372             }
373             elsif ( $what_now eq 's' ) {
374 0           return 0;
375             }
376             elsif ( $what_now eq 'b' ) {
377 0           info(HR_LIGHT);
378 0           info('%s', $self->blame_violation(
379             $session,
380             $rel_filename,
381             $violation->line_number
382             ));
383 0           goto RETRY;
384             }
385             }
386              
387 0           return 0;
388             }
389              
390             1;
391              
392             =pod
393              
394             =head1 NAME
395              
396             App::Critique::Command::process - Critique all the files.
397              
398             =head1 VERSION
399              
400             version 0.05
401              
402             =head1 DESCRIPTION
403              
404             This command will start or resume the critique session, allowing you to
405             step through the files and critique them. This current state of this
406             processing will be stored in the critique session file and so can be
407             stopped and resumed at any time.
408              
409             Note, this is an interactive command, so ...
410              
411             =head1 AUTHOR
412              
413             Stevan Little <stevan@cpan.org>
414              
415             =head1 COPYRIGHT AND LICENSE
416              
417             This software is copyright (c) 2016 by Stevan Little.
418              
419             This is free software; you can redistribute it and/or modify it under
420             the same terms as the Perl 5 programming language system itself.
421              
422             =cut
423              
424             __END__
425              
426             # ABSTRACT: Critique all the files.
427