File Coverage

blib/lib/Test2/Tools/PerlCritic.pm
Criterion Covered Total %
statement 230 232 99.1
branch 57 58 98.2
condition 12 18 66.6
subroutine 34 34 100.0
pod 2 5 40.0
total 335 347 96.5


line stmt bran cond sub pod time code
1             package Test2::Tools::PerlCritic;
2              
3 1     1   226977 use strict;
  1         3  
  1         92  
4 1     1   58 use warnings;
  1         3  
  1         111  
5 1     1   8 use Exporter qw( import );
  1         2  
  1         67  
6 1     1   28 use 5.020;
  1         5  
7 1     1   687 use experimental qw( postderef signatures );
  1         7416  
  1         7  
8 1     1   252 use Carp qw( croak );
  1         2  
  1         91  
9 1     1   661 use Ref::Util qw( is_ref is_plain_arrayref is_plain_hashref is_blessed_ref is_plain_coderef );
  1         3160  
  1         140  
10 1     1   17 use Test2::API qw( context );
  1         7  
  1         69  
11 1     1   1490 use Perl::Critic ();
  1         2226759  
  1         38  
12 1     1   11 use Perl::Critic::Utils ();
  1         2  
  1         23  
13 1     1   1041 use Path::Tiny ();
  1         18000  
  1         49  
14 1     1   13 use Class::Tiny qw( critic test_name files _hooks );
  1         2  
  1         15  
15              
16             our @EXPORT = qw( perl_critic_ok );
17              
18             # ABSTRACT: Testing tools to enforce Perl::Critic policies
19             our $VERSION = '0.08'; # VERSION
20              
21              
22             sub BUILDARGS
23             {
24 26     26 0 5990351   my $class = shift; # unused
25              
26 26 100 66     198   if(is_plain_hashref $_[0] && @_ == 1)
27               {
28 7         30     return $_[0];
29               }
30              
31 19         50   my $files = shift;
32 19         59   my @opts;
33               my $critic;
34              
35 19 100 100     104   if(defined $_[0] && is_ref $_[0]) {
36 7 100       162     if(is_plain_arrayref $_[0])
    100          
    100          
37                 {
38 1         2       @opts = @{ shift() };
  1         4  
39                 }
40                 elsif(is_plain_hashref $_[0])
41                 {
42 1         4       @opts = %{ shift() };
  1         45  
43                 }
44 5         44     elsif(eval { $_[0]->isa('Perl::Critic') })
45                 {
46 4         8       $critic = shift;
47                 }
48                 else
49                 {
50 1         194       croak "options must be either an array or hash reference";
51                 }
52               }
53              
54 18   66     174   $critic ||= Perl::Critic->new(@opts);
55              
56 18         12671983   my $test_name = shift;
57              
58               return {
59 18         215     files => $files,
60                 critic => $critic,
61                 test_name => $test_name,
62               };
63             }
64              
65 25         74 sub BUILD ($self, $)
66 25     25 0 1028 {
  25         50  
67 25         913   my $files = $self->files;
68              
69 25 100       235   if(defined $files)
70               {
71 24 100       93     if(is_ref $files)
72                 {
73 3 50       19       unless(is_plain_arrayref $files)
74                   {
75 0         0         croak "file argument muse be a file/directory name or and array of reference of file/directory names";
76                   }
77                 }
78                 else
79                 {
80 21         62       $files = [$files];
81                 }
82              
83 24         98     @$files = map { "$_" } @$files;
  25         112  
84              
85               }
86               else
87               {
88 1         289     croak "no files provided";
89               }
90              
91 24 100       624   unless(defined $self->test_name)
92               {
93 21         613     $self->test_name("no Perl::Critic policy violations for @$files");
94               }
95              
96 105         47566   @$files = sort map { Path::Tiny->new($_)->stringify } map {
97 24 100       248     -f $_
  25 100       1358  
98                   ? $_
99                   : -d $_
100                     ? Perl::Critic::Utils::all_perl_files("$_")
101                     : croak "not a file or directory: $_";
102               } @$files;
103              
104 23         2127   $self->files($files);
105              
106 23         839   $self->_hooks({
107                 cleanup => [],
108               });
109             }
110              
111 23         104 sub DEMOLISH ($self, $global)
112 23     23 0 42358 {
  23         85  
  23         45  
113 23         785   $_->($self, $global) for $self->_hooks->{cleanup}->@*;
114             }
115              
116              
117             sub perl_critic_ok
118             {
119 6 100 66 6 1 950814   my $self = is_blessed_ref($_[0]) && $_[0]->isa(__PACKAGE__) ? $_[0] : __PACKAGE__->new(@_);
120              
121 6         58   my %violations;
122              
123 6         200   foreach my $file ($self->files->@*)
124               {
125 14         738     my @critic_violations = $self->critic->critique($file);
126 14 100       121393     next unless @critic_violations;
127              
128 12 100       554     if(my $hooks = $self->_hooks->{violations})
129                 {
130 3         37       $_->($self, @critic_violations) for @$hooks;
131                 }
132              
133 12         154     foreach my $critic_violation (@critic_violations)
134                 {
135 12         67       my $policy = $critic_violation->policy;
136 12   66     225       my $violation = $violations{$policy} //= Test2::Tools::PerlCritic::Violation->new($critic_violation);
137 12         231       $violation->add_file_location($critic_violation);
138                 }
139               }
140              
141 6         158   my $ok = 1;
142 6         22   my @diag;
143               my @note;
144              
145 6 100       28   if(%violations)
146               {
147 4         16     foreach my $violation (sort { $a->policy cmp $b->policy } values %violations)
  0         0  
148                 {
149 4 100       127       if(my $hook = $self->_hooks->{progressive_check})
150                   {
151 2         25         $violation->progressive_check($self, $hook);
152                   }
153 4         74       push @diag, $violation->diag;
154 4         21       push @note, $violation->note;
155 4 100       19       $ok = 0 unless $violation->ok;
156                 }
157               }
158              
159 6         91   my $ctx = context();
160 6 100       1340   if($ok)
161               {
162 3         107     $ctx->pass($self->test_name);
163 3         468     $ctx->diag($_) for @diag;
164               }
165               else
166               {
167 3         102     $ctx->fail($self->test_name, @diag);
168               }
169              
170 6 100       1960   if(@note)
171               {
172 2         14     $ctx->note("### The following violations were grandfathered from before ###");
173 2         739     $ctx->note("### these polcies were applied and so should be fixed only ###");
174 2         532     $ctx->note("### when practical ###");
175 2         518     $ctx->note($_) for @note;
176               }
177              
178 6         3646   $ctx->release;
179             }
180              
181              
182 9         25 sub add_hook ($self, $name, $sub)
  9         24  
183 9     9 1 183 {
  9         19  
  9         23  
184 9 100       70   if($name =~ /^(?:progressive_check|cleanup|violations)$/)
185               {
186 8 100       65     if(is_plain_coderef($sub))
187                 {
188 7 100       39       if($name =~ /^(?:cleanup|violations)$/)
189                   {
190 3         85         push $self->_hooks->{$name}->@*, $sub;
191                   }
192                   else
193                   {
194 4 100       111         croak "Only one $name hook allowed" if defined $self->_hooks->{$name};
195 3         96         $self->_hooks->{$name} = $sub;
196                   }
197 6         57       return $self;
198                 }
199 1         246     croak "hook is not a code reference";
200               }
201 1         314   croak "unknown hook: $name";
202             }
203              
204             package Test2::Tools::PerlCritic::Violation;
205              
206 1     1   4117 use Class::Tiny qw( severity description diagnostics policy files );
  1         4  
  1         9  
207              
208 4         13 sub BUILDARGS ($class, $violation)
209 4     4   182 {
  4         10  
  4         9  
210 4         13   my %args = map { $_ => $violation->$_ } qw( severity description diagnostics policy );
  16         2031  
211 4         46   $args{files} = {};
212 4         22   return \%args;
213             }
214              
215 12         38 sub add_file_location ($self, $violation)
216 12     12   30 {
  12         23  
  12         24  
217 12   33     388   my $file = $self->files->{$violation->logical_filename} //= Test2::Tools::PerlCritic::File->new($violation);
218 12         220   $file->add_location($violation);
219             }
220              
221             sub _chomp ($str)
222 5     5   36 {
  5         11  
  5         9  
223 5         18   chomp $str;
224 5         14   return $str;
225             }
226              
227             sub _text ($self)
228 5     5   11 {
  5         11  
  5         11  
229 5         10   my @txt;
230 5         17   push @txt, '';
231 5         129   push @txt, sprintf("%s [sev %s]", $self->policy, $self->severity);
232 5         298   push @txt, $self->description;
233 5         132   push @txt, _chomp($self->diagnostics);
234 5         46   push @txt, '';
235 5         25   return @txt;
236             }
237              
238             sub diag ($self)
239 4     4   11 {
  4         11  
  4         9  
240 4         8   my @diag;
241              
242 4         8   my $first = 1;
243              
244 4         129   foreach my $file (sort { $a->logical_filename cmp $b->logical_filename } values $self->files->%*)
  12         373  
245               {
246 12 100       962     next if $file->progressive_allowed;
247 8         233     foreach my $location ($file->locations->@*)
248                 {
249 8 100       70       push @diag, $self->_text if $first;
250 8         17       $first = 0;
251              
252 8         222       push @diag, sprintf("found at %s line %s column %s",
253                     Path::Tiny->new($file->logical_filename)->stringify,
254                     $location->logical_line_number,
255                     $location->visual_column_number,
256                   );
257                 }
258               }
259              
260 4         392   return @diag;
261             }
262              
263             sub note ($self)
264 4     4   10 {
  4         9  
  4         8  
265 4         10   my @diag;
266              
267 4         9   my $first = 1;
268              
269 4         102   foreach my $file (sort { $a->logical_filename cmp $b->logical_filename } values $self->files->%*)
  12         325  
270               {
271 12 100       650     next unless $file->progressive_allowed;
272 4         103     foreach my $location ($file->locations->@*)
273                 {
274 4 100       53       push @diag, $self->_text if $first;
275 4         9       $first = 0;
276              
277 4         90       push @diag, sprintf("found at %s line %s column %s",
278                     Path::Tiny->new($file->logical_filename)->stringify,
279                     $location->logical_line_number,
280                     $location->visual_column_number,
281                   );
282                 }
283               }
284              
285 4         167   return @diag;
286             }
287              
288             sub ok ($self)
289 4     4   10 {
  4         9  
  4         23  
290 4         98   foreach my $file (values $self->files->%*)
291               {
292 6 100       53     return 0 unless $file->ok;
293               }
294 1         11   return 1;
295             }
296              
297 2         5 sub progressive_check ($self, $test_critic, $code)
  2         4  
298 2     2   6 {
  2         5  
  2         7  
299 2         50   foreach my $file (values $self->files->%*)
300               {
301 6 100       183     if($code->($test_critic, $self->policy, $file->logical_filename, $file->count))
302                 {
303 4         140       $file->progressive_allowed(1);
304                 }
305               }
306             }
307              
308             package Test2::Tools::PerlCritic::File;
309              
310 1     1   2553 use Class::Tiny qw( logical_filename locations progressive_allowed );
  1         3  
  1         7  
311              
312 12         28 sub BUILDARGS ($class, $violation)
313 12     12   644 {
  12         27  
  12         27  
314 12         26   my %args;
315 12         69   $args{logical_filename} = $violation->logical_filename;
316 12         154   $args{locations} = [];
317 12         46   return \%args;
318             }
319              
320 12         30 sub BUILD ($self, $)
321 12     12   381 {
  12         35  
322 12         396   $self->progressive_allowed(0);
323             }
324              
325 12         28 sub add_location ($self, $violation)
326 12     12   29 {
  12         25  
  12         25  
327 12         356   push $self->locations->@*, Test2::Tools::PerlCritic::Location->new($violation);
328             }
329              
330             sub count ($self)
331 6     6   200 {
  6         13  
  6         9  
332 6         135   scalar $self->locations->@*;
333             }
334              
335             sub ok ($self)
336 6     6   13 {
  6         13  
  6         60  
337 6         140   return !!$self->progressive_allowed;
338             }
339              
340             package Test2::Tools::PerlCritic::Location;
341              
342 1     1   1112 use Class::Tiny qw( logical_line_number visual_column_number );
  1         10  
  1         6  
343              
344 12         32 sub BUILDARGS ($class, $violation)
345 12     12   368 {
  12         26  
  12         32  
346 12         67   my %args = map { $_ => $violation->$_ } qw( logical_line_number visual_column_number );
  24         269  
347 12         176   return \%args;
348             }
349              
350             1;
351              
352             __END__
353            
354             =pod
355            
356             =encoding UTF-8
357            
358             =head1 NAME
359            
360             Test2::Tools::PerlCritic - Testing tools to enforce Perl::Critic policies
361            
362             =head1 VERSION
363            
364             version 0.08
365            
366             =head1 SYNOPSIS
367            
368             Original procedural interface:
369            
370             use Test2::V0;
371             use Test2::Tools::PerlCritic;
372            
373             perl_critic_ok ['lib','t'], 'test library files';
374            
375             done_testing;
376            
377             New OO interface:
378            
379             use Test2::V0;
380             use Test2::Tools::PerlCritic ();
381             use Perl::Critic;
382            
383             my $test_critic = Test2::Tools::PerlCritic->new({
384             files => ['lib','t'],
385             test_name => 'test library_files',
386             });
387            
388             $test_critic->perl_critic_ok;
389            
390             done_testing;
391            
392             =head1 DESCRIPTION
393            
394             Test for L<Perl::Critic> violations using L<Test2>. Although this testing
395             tool uses the L<Test2> API instead of the older L<Test::Builder> API, the primary
396             motivation is to provide output in a more useful form. That is policy violations
397             are grouped by policy class, and the policy class name is clearly displayed as
398             a diagnostic. The author finds the former more useful because he tends to address
399             one type of violation at a time. The author finds the latter more useful because
400             he tends to want to lookup or adjust the configuration of the policy as he is
401             addressing violations.
402            
403             =head1 FUNCTIONS
404            
405             =head2 perl_critic_ok
406            
407             perl_critic_ok $path, \@options, $test_name;
408             perl_critic_ok \@path, \@options, $test_name;
409             perl_critic_ok $path, \%options, $test_name;
410             perl_critic_ok \@path, \%options, $test_name;
411             perl_critic_ok $path, $critic, $test_name;
412             perl_critic_ok \@path, $critic, $test_name;
413             perl_critic_ok $path, $test_name;
414             perl_critic_ok \@path, $test_name;
415             perl_critic_ok $path;
416             perl_critic_ok \@path;
417            
418             Run L<Perl::Critic> on the given files or directories. The first argument
419             (C<$path> or C<\@path>) can be either the path to a file or directory, or
420             a array reference to a list of paths to files and directories. If C<\@options> or
421             C<\%options> are provided, then they will be passed into the
422             L<Perl::Critic> constructor. If C<$critic> (an instance of L<Perl::Critic>)
423             is provided, then that L<Perl::Critic> instance will be used instead
424             of creating one internally. Finally the C<$test_name> may be provided
425             if you do not like the default test name.
426            
427             Only a single test is run regardless of how many files are processed.
428             this is so that the policy violations can be grouped by policy class
429             across multiple files.
430            
431             As a convenience, if the test passes then a true value is returned.
432             Otherwise a false will be returned.
433            
434             C<done_testing> or the equivalent is NOT called by this function.
435             You are responsible for calling that yourself.
436            
437             Since we do not automatically call C<done_testing>, you can call C<perl_critic_ok>
438             multiple times, but keep in mind that the policy violations will only be grouped
439             in each individual call, so it is probably better to provide a list of paths,
440             rather than make multiple calls.
441            
442             =head1 CONSTRUCTOR
443            
444             my $test_critic = Test2::Tools::PerlCritic->new(\%properties);
445            
446             Properties:
447            
448             =over 4
449            
450             =item files
451            
452             (REQUIRED)
453            
454             List of files or directories. Directories will be recursively searched for
455             Perl files (C<.pm>, C<.pl> and C<.t>).
456            
457             =item critic
458            
459             The L<Perl::Critic> instance. One will be created if not provided.
460            
461             =item test_name
462            
463             The name of the test. This is used in diagnostics.
464            
465             =back
466            
467             =head1 METHODS
468            
469             =head2 perl_critic_ok
470            
471             $test_critic->perl_critic_ok;
472            
473             The method version works just like the functional version above,
474             except it doesn't take any additional arguments.
475            
476             =head2 add_hook
477            
478             $test_critic->add_hook($hook_name, \&code);
479            
480             Adds the given hook. Available hooks:
481            
482             =over 4
483            
484             =item cleanup
485            
486             $test_critic->add_hook(cleanup => sub ($test_critic, $global) {
487             ...
488             });
489            
490             This hook is called when the L<Test2::Tools::PerlCritic> instance is destroyed.
491            
492             If the hook is called during global destruction of the Perl interpreter,
493             C<$global> will be set to a true value.
494            
495             This hook can be set multiple times.
496            
497             =item progressive_check
498            
499             $test_critic->add_hook(progressive_check => sub ($test_critic, $policy, $file, $count) {
500             ...
501             return $bool;
502             });
503            
504             This hook is made available for violations in existing code when new policies
505             are added. Passed in are the L<Test2::Tools::PerlCritic> instance, the policy
506             name, the filename and the number of times the violation was found. If the
507             violations are from an old code base with grandfathered allowed violations,
508             this hook should return true, and the violation will be reported as a C<note>
509             instead of C<diag> and will not cause the test as a whole to fail. Otherwise
510             the violation will be reported using C<diag> and the test as a whole will fail.
511            
512             This hook can only be set once.
513            
514             =item violations
515            
516             $test_critic->add_hook(violations => sub ($test_critic, @violations) {
517             ...
518             });
519            
520             Each time violations are returned from L<Perl::Critic/critique>, they are
521             passed into this hook as a list. The order and grouping of violations
522             may change in the future.
523            
524             =back
525            
526             =head1 CAVEATS
527            
528             L<Test::Perl::Critic> has been around longer, and probably does at least some things smarter.
529             The fact that this module groups policy violations for all files by class means that it has
530             to store more diagnostics in memory before sending them out I<en masse>, where as
531             L<Test::Perl::Critic> sends violations for each file as it processes them. L<Test::Perl::Critic>
532             also comes with some code to optionally do processing in parallel. Some of these issues may
533             or may not be addressed in future versions of this module.
534            
535             Since this module formats it's output the C<-verbose> option is ignored at the C<set_format>
536             value is ignored.
537            
538             =head1 SEE ALSO
539            
540             =over 4
541            
542             =item L<Test::Perl::Critic>
543            
544             =item L<Perl::Critic>
545            
546             =back
547            
548             =head1 AUTHOR
549            
550             Graham Ollis <plicease@cpan.org>
551            
552             =head1 COPYRIGHT AND LICENSE
553            
554             This software is copyright (c) 2019-2024 by Graham Ollis.
555            
556             This is free software; you can redistribute it and/or modify it under
557             the same terms as the Perl 5 programming language system itself.
558            
559             =cut
560