File Coverage

blib/lib/Git/Critic.pm
Criterion Covered Total %
statement 72 131 54.9
branch 5 40 12.5
condition 0 3 0.0
subroutine 23 27 85.1
pod 1 1 100.0
total 101 202 50.0


line stmt bran cond sub pod time code
1             package Git::Critic;
2              
3             # ABSTRACT: Only run Perl::Critic on lines changed in the current branch
4 1     1   99080 use v5.10.0;
  1         13  
5 1     1   4 use strict;
  1         2  
  1         15  
6 1     1   4 use warnings;
  1         1  
  1         18  
7             # we don't use this directly, but it's an attempt to fix this issue:
8             # IPC::System::Simple required for Fatalised/autodying system() at .../lib/Git/Critic.pm line 7.
9 1     1   811 use IPC::System::Simple;
  1         10565  
  1         40  
10 1     1   416 use autodie ":all";
  1         11779  
  1         4  
11              
12 1     1   10015 use Capture::Tiny 'capture_stdout';
  1         19155  
  1         46  
13 1     1   5 use Carp;
  1         2  
  1         41  
14 1     1   4 use File::Basename 'basename';
  1         2  
  1         70  
15 1     1   5 use File::Temp 'tempfile';
  1         2  
  1         34  
16 1     1   4 use List::Util 1.44 qw(uniq);
  1         17  
  1         44  
17 1     1   459 use Moo;
  1         9257  
  1         4  
18 1     1   1796 use Types::Standard qw( ArrayRef Bool Int Str);
  1         63325  
  1         10  
19              
20             our $VERSION = '0.7';
21              
22             #
23             # Moo attributes
24             #
25              
26             has primary_target => (
27             is => 'ro',
28             isa => Str,
29             required => 1,
30             );
31              
32             has current_target => (
33             is => 'ro',
34             isa => Str,
35             lazy => 1,
36             builder => '_build_current_target',
37             );
38              
39             has max_file_size => (
40             is => 'ro',
41             isa => Int,
42             default => 0,
43             );
44              
45             has severity => (
46             is => 'ro',
47             isa => Int | Str,
48             default => 5,
49             );
50              
51             has profile => (
52             is => 'ro',
53             isa => Str,
54             default => '',
55             );
56              
57             has verbose => (
58             is => 'ro',
59             isa => Bool,
60             default => 0,
61             );
62              
63             # this is only for tests
64             has _run_test_queue => (
65             is => 'ro',
66             isa => ArrayRef,
67             default => sub { [] },
68             init_arg => undef,
69             );
70              
71             #
72             # Builders
73             #
74              
75             sub _build_current_target {
76 1     1   31 my $self = shift;
77 1         4 return $self->_run( 'git', 'rev-parse', '--abbrev-ref', 'HEAD' );
78             }
79              
80             #
81             # The following methods are for the tests
82             #
83              
84             # return true if we have any data in our test queue
85             sub _run_queue_active {
86 3     3   6 my $self = shift;
87 3         3 return scalar @{ $self->_run_test_queue };
  3         9  
88             }
89              
90             sub _add_to_run_queue {
91 3     3   2488 my ( $self, $result ) = @_;
92 3         4 push @{ $self->_run_test_queue } => $result;
  3         11  
93             }
94              
95             sub _get_next_run_queue_response {
96 3     3   4 my $self = shift;
97 3         3 shift @{ $self->_run_test_queue };
  3         59  
98             }
99              
100             #
101             # These call system commands
102             #
103              
104             # if we have a response added to the run queue via _add_to_run_queue, return
105             # that instead of calling the system command. Let it die if the system command
106             # fails
107              
108             sub _run {
109 3     3   10 my ( $self, @command ) = @_;
110 3 50       7 if ( $self->_run_queue_active ) {
111 3         7 return $self->_get_next_run_queue_response;
112             }
113              
114 0 0       0 if ( $self->verbose ) {
115 0         0 say STDERR "Running command: @command";
116             }
117              
118             # XXX yeah, this needs to be more robust
119 0     0   0 chomp( my $result = capture_stdout { system(@command) } );
  0         0  
120 0         0 return $result;
121             }
122              
123             # same as _run, but don't let it die
124             sub _run_without_die {
125 0     0   0 my ( $self, @command ) = @_;
126 0 0       0 if ( $self->verbose ) {
127 0         0 say STDERR "Running command: @command";
128             }
129             chomp(
130             my $result = capture_stdout {
131 1     1   1329 no autodie;
  1         2  
  1         7  
132 0     0   0 system(@command);
133             }
134 0         0 );
135 0         0 return $result;
136             }
137              
138             # get Perl files which have been changed in the current branch
139             sub _get_modified_perl_files {
140 1     1   6 my $self = shift;
141 1         3 my $primary_target = $self->primary_target;
142 1         21 my $current_target = $self->current_target;
143 1 50       11 my @files = uniq sort grep { /\S/ && $self->_is_perl($_) }
  3         15  
144             split /\n/ => $self->_run( 'git', 'diff', '--name-only',
145             "$primary_target...$current_target" );
146 1         7 return @files;
147             }
148              
149             # get the diff of the current file
150             sub _get_diff {
151 1     1   7 my ( $self, $file ) = @_;
152 1         3 my $primary_target = $self->primary_target;
153 1         21 my $current_target = $self->current_target;
154 1         10 my @diff =
155             split /\n/ =>
156             $self->_run( 'git', 'diff', "$primary_target...$current_target", $file );
157 1         16 return @diff;
158             }
159              
160             # remove undefined arguments. This makes a command line
161             # script easier to follow
162             around BUILDARGS => sub {
163             my ( $orig, $class, @args ) = @_;
164              
165             my $arg_for = $class->$orig(@args);
166             foreach my $arg ( keys %$arg_for ) {
167             if ( not defined $arg_for->{$arg} ) {
168             delete $arg_for->{$arg};
169             }
170             }
171             return $arg_for;
172             };
173              
174             sub run {
175 0     0 1 0 my $self = shift;
176              
177 0         0 my $primary_target = $self->primary_target;
178 0         0 my $current_target = $self->current_target;
179 0 0       0 if ( $primary_target eq $current_target ) {
180              
181             # in the future, we might want to allow you to check the primary
182             # branch X commits back
183 0         0 return;
184             }
185              
186             # We walking through every file you've changed and parse the diff to
187             # figure out the start and end of every change you've made. Any perlcritic
188             # failures which are *not* on those lines are ignored
189 0         0 my @files = $self->_get_modified_perl_files;
190 0         0 my @failures;
191 0         0 FILE: foreach my $file (@files) {
192 0         0 my %reported;
193              
194 0 0       0 my $file_text = $self->_run( 'git', 'show', "${current_target}:$file" )
195             or next FILE;
196 0 0       0 if ( $self->max_file_size ) {
197             # we want the length in bytes, not characters
198 1     1   2897 use bytes;
  1         2  
  1         8  
199             next FILE
200 0 0       0 unless length($file_text) <= $self->max_file_size; # large files are very slow
201             }
202              
203 0         0 my ($fh, $filename) = tempfile();
204 0         0 print $fh $file_text;
205 0         0 close $fh;
206 0         0 my $severity = $self->severity;
207 0         0 my $profile = $self->profile;
208 0         0 my @arguments = ("--severity=$severity");
209 0 0       0 push @arguments, "--profile=$profile" if $profile;
210 0         0 push @arguments, $filename;
211 0         0 my $critique =
212             $self->_run_without_die( 'perlcritic', @arguments );
213 0 0       0 next FILE unless $critique; # should never happen unless perlcritic dies
214 0         0 my @critiques = split /\n/, $critique;
215              
216             # unified diff format
217             # @@ -3,8 +3,9 @@
218             # @@ from-file-line-numbers to-file-line-numbers @@
219             my @chunks = map {
220 0         0 /^ \@\@\s+ -\d+,\d+\s+
221             \+(?\d+)
222             ,(?\d+)
223             \s+\@\@/xs
224 1 0   1   560 ? [ $+{start}, $+{start} + $+{lines} ]
  1         343  
  1         418  
  0         0  
225             : ()
226             } $self->_get_diff($file);
227 0         0 my $max_line_number = $chunks[-1][-1];
228 0         0 CRITIQUE: foreach my $this_critique (@critiques) {
229 0 0       0 next CRITIQUE if $this_critique =~ / source OK$/;
230 0         0 $this_critique =~ /\bline\s+(?\d+)/;
231 0 0       0 unless ( defined $+{line_number} ) {
232 0         0 warn "Could not find line number in critique $this_critique";
233 0         0 next;
234             }
235              
236             # no need to keep processing
237 0 0       0 last CRITIQUE if $+{line_number} > $max_line_number;
238              
239 0         0 foreach my $chunk (@chunks) {
240 0         0 my ( $min, $max ) = @$chunk;
241 0 0 0     0 if ( $+{line_number} >= $min && $+{line_number} <= $max ) {
242 0 0       0 unless ($reported{$this_critique}) {
243 0         0 push @failures => "$file: $this_critique"
244             }
245 0         0 $reported{$this_critique}++;
246 0         0 next CRITIQUE;
247             }
248             }
249             }
250             }
251 0         0 return @failures;
252             }
253              
254             # a heuristic to determine if the file in question is Perl. We might allow
255             # a client to override this in the future
256             sub _is_perl {
257 3     3   7 my ( $self, $file ) = @_;
258 3 100       70 return unless -e $file; # sometimes we get non-existent files
259 2 50       20 return 1 if $file =~ /\.(?:p[ml]|t)$/;
260              
261             # if we got to here, let's check to see if "perl" is in a shebang
262 0           open my $fh, '<', $file;
263 0           my $first_line = <$fh>;
264 0           close $fh;
265 0 0         if ( $first_line =~ /^#!.*\bperl\b/ ) {
266 0 0         say STDERR "Found changed Perl file: $file" if $self->verbose;
267 0           return $file;
268             }
269 0           return;
270             }
271              
272             # vim: filetype=perl
273              
274             1;
275              
276             __END__