File Coverage

blib/lib/Code/TidyAll.pm
Criterion Covered Total %
statement 328 389 84.3
branch 94 156 60.2
condition 31 49 63.2
subroutine 67 73 91.7
pod 7 12 58.3
total 527 679 77.6


line stmt bran cond sub pod time code
1             package Code::TidyAll;
2              
3 27     27   1410498 use strict;
  27         122  
  27         814  
4 27     27   171 use warnings;
  27         64  
  27         818  
5              
6 27     27   11462 use Code::TidyAll::Cache;
  27         117  
  27         1092  
7 27     27   13321 use Code::TidyAll::CacheModel;
  27         115  
  27         1114  
8 27     27   14143 use Code::TidyAll::Config::INI::Reader;
  27         108  
  27         1117  
9 27     27   12321 use Code::TidyAll::Plugin;
  27         107  
  27         1124  
10 27     27   11933 use Code::TidyAll::Result;
  27         115  
  27         1214  
11 27     27   12972 use Code::TidyAll::Zglob qw(zglob);
  27         78  
  27         1721  
12 27     27   2078 use Data::Dumper;
  27         21259  
  27         1233  
13 27     27   12552 use Date::Format;
  27         198979  
  27         2119  
14 27     27   235 use Digest::SHA qw(sha1_hex);
  27         71  
  27         1552  
15 27     27   188 use File::Find qw(find);
  27         71  
  27         2003  
16 27     27   11696 use File::pushd qw( pushd );
  27         32549  
  27         1626  
17 27     27   13461 use List::SomeUtils qw(uniq);
  27         220328  
  27         2159  
18 27     27   247 use Module::Runtime qw( use_module );
  27         88  
  27         202  
19 27     27   1754 use Path::Tiny qw(path);
  27         77  
  27         1251  
20 27     27   169 use Scalar::Util qw(blessed);
  27         62  
  27         1225  
21 27     27   164 use Specio 0.40;
  27         471  
  27         775  
22 27     27   159 use Specio::Declare;
  27         60  
  27         214  
23 27     27   5631 use Specio::Library::Builtins;
  27         75  
  27         463  
24 27     27   260338 use Specio::Library::Numeric;
  27         77  
  27         244  
25 27     27   226376 use Specio::Library::Path::Tiny 0.04;
  27         943  
  27         211  
26 27     27   382493 use Specio::Library::String;
  27         71  
  27         246  
27 27     27   75673 use Time::Duration::Parse qw(parse_duration);
  27         56274  
  27         196  
28 27     27   2272 use Try::Tiny;
  27         70  
  27         1651  
29              
30 27     27   174 use Moo 2.000000;
  27         672  
  27         239  
31              
32             our $VERSION = '0.83';
33              
34 0     0 0 0 sub default_conf_names { ( 'tidyall.ini', '.tidyallrc' ) }
35              
36             # External
37             has backup_ttl => (
38             is => 'ro',
39             isa => t('NonEmptyStr'),
40             default => '1 hour',
41             );
42              
43             has cache => (
44             is => 'lazy',
45             isa => object_can_type( methods => [qw( get set )] ),
46             );
47              
48             has cache_model_class => (
49             is => 'ro',
50             isa => t('ClassName'),
51             default => 'Code::TidyAll::CacheModel',
52             );
53              
54             has check_only => (
55             is => 'ro',
56             isa => t('Bool'),
57             );
58              
59             has data_dir => (
60             is => 'lazy',
61             isa => t('Path'),
62             coerce => t('Path')->coercion_sub,
63             );
64              
65             has iterations => (
66             is => 'ro',
67             isa => t('PositiveInt'),
68             default => 1,
69             );
70              
71             has jobs => (
72             is => 'ro',
73             isa => t('Int'),
74             default => 1,
75             );
76              
77             has list_only => (
78             is => 'ro',
79             isa => t('Bool'),
80             );
81              
82             has mode => (
83             is => 'ro',
84             isa => t('NonEmptyStr'),
85             default => 'cli',
86             );
87              
88             has msg_outputter => (
89             is => 'ro',
90             isa => t('CodeRef'),
91             builder => '_build_msg_outputter',
92             );
93              
94             has no_backups => (
95             is => 'ro',
96             isa => t('Bool'),
97             );
98              
99             has no_cache => (
100             is => 'ro',
101             isa => t('Bool'),
102             );
103              
104             has output_suffix => (
105             is => 'ro',
106             isa => t('Str'),
107             default => q{},
108             );
109              
110             has plugins => (
111             is => 'ro',
112             isa => t('HashRef'),
113             required => 1,
114             );
115              
116             has selected_plugins => (
117             is => 'ro',
118             isa => t( 'ArrayRef', of => t('NonEmptyStr') ),
119             lazy => 1,
120             default => sub { [] },
121             );
122              
123             has quiet => (
124             is => 'ro',
125             isa => t('Bool'),
126             );
127             has recursive => (
128             is => 'ro',
129             isa => t('Bool'),
130             );
131              
132             has refresh_cache => (
133             is => 'ro',
134             isa => t('Bool'),
135             );
136              
137             has root_dir => (
138             is => 'ro',
139             isa => t('RealDir'),
140             coerce => t('RealDir')->coercion_sub,
141             required => 1
142             );
143              
144             has verbose => (
145             is => 'ro',
146             isa => t('Bool'),
147             default => 0,
148             );
149              
150             has inc => (
151             is => 'ro',
152             isa => t( 'ArrayRef', of => t('NonEmptyStr') ),
153             default => sub { [] },
154             );
155              
156             has _backup_dir => (
157             is => 'ro',
158             isa => t('Path'),
159             init_arg => undef,
160             lazy => 1,
161             builder => '_build_backup_dir',
162             );
163              
164             has _backup_ttl_secs => (
165             is => 'ro',
166             isa => t('Int'),
167             init_arg => undef,
168             lazy => 1,
169             builder => '_build_backup_ttl_secs',
170             );
171              
172             has _base_sig => (
173             is => 'ro',
174             isa => t('NonEmptyStr'),
175             init_arg => undef,
176             lazy => 1,
177             builder => '_build_base_sig',
178             );
179              
180             has _plugin_objects => (
181             is => 'ro',
182             isa => t( 'ArrayRef', of => object_isa_type('Code::TidyAll::Plugin') ),
183             init_arg => undef,
184             lazy => 1,
185             builder => '_build_plugin_objects',
186             );
187              
188             has _plugins_to_run => (
189             is => 'ro',
190             isa => t( 'HashRef', of => t('HashRef') ),
191             init_arg => undef,
192             lazy => 1,
193             builder => '_build_plugins_to_run',
194             );
195              
196             has _plugins_for_path => (
197             is => 'ro',
198             isa => t( 'HashRef', of => t('HashRef') ),
199             init_arg => undef,
200             lazy => 1,
201             default => sub { {} },
202             );
203              
204             with qw( Code::TidyAll::Role::HasIgnore Code::TidyAll::Role::Tempdir );
205              
206             sub _build_backup_dir {
207 65     65   869 my $self = shift;
208 65         1282 return $self->data_dir->child('backups');
209             }
210              
211             sub _build_backup_ttl_secs {
212 65     65   816 my $self = shift;
213 65         484 return parse_duration( $self->backup_ttl );
214             }
215              
216             sub _build_base_sig {
217 35     35   2394 my $self = shift;
218 35         77 my $active_plugins = join( q{|}, map { $_->name } @{ $self->_plugin_objects } );
  50         1420  
  35         754  
219 35   50     291 return $self->_sig( [ $Code::TidyAll::VERSION || 0, $active_plugins ] );
220             }
221              
222             sub _sig {
223 35     35   101 my ( $self, $data ) = @_;
224 35         1003 return sha1_hex( join( ',', @$data ) );
225             }
226              
227             sub _build_cache {
228 67     67   898 my $self = shift;
229 67         1133 return Code::TidyAll::Cache->new( cache_dir => $self->data_dir->child('cache') );
230             }
231              
232             sub _build_data_dir {
233 69     69   5464 my $self = shift;
234 69         385 return $self->root_dir->child('/.tidyall.d');
235             }
236              
237             sub _build_plugins_to_run {
238 66     66   719 my $self = shift;
239              
240 66         197 my $all_plugins = $self->plugins;
241 66         126 my %selected = map { $_ => 1 } @{ $self->selected_plugins };
  6         37  
  66         1271  
242 66         3055 my %plugins;
243              
244 66 100       468 if (%selected) {
    50          
245 2         8 my @unknown = sort grep { !$all_plugins->{$_} } keys %selected;
  6         18  
246 2 100       78 die "Asked for unknown plugins: [@unknown]" if @unknown;
247 1         6 %plugins = map { $_ => $all_plugins->{$_} } keys %selected;
  2         6  
248             }
249             elsif ( my $mode = $self->mode ) {
250 79         300 %plugins = map { $_ => $all_plugins->{$_} }
251 80         253 grep { $self->_plugin_conf_matches_mode( $all_plugins->{$_}, $mode ) }
252 64         140 keys %{$all_plugins};
  64         221  
253             }
254              
255 65         1408 return \%plugins;
256             }
257              
258             sub _plugin_conf_matches_mode {
259 80     80   208 my ( $self, $conf, $mode ) = @_;
260              
261 80 100       327 if ( my $only_modes = $conf->{only_modes} ) {
262 2 100       33 return 0 if ( q{ } . $only_modes . q{ } ) !~ / $mode /;
263             }
264 79 50       283 if ( my $except_modes = $conf->{except_modes} ) {
265 0 0       0 return 0 if ( q{ } . $except_modes . q{ } ) =~ / $mode /;
266             }
267 79         249 return 1;
268             }
269              
270             sub _build_plugin_objects {
271 66     66   1019 my $self = shift;
272              
273             # Sort tidiers by weight (by default validators have a weight of 60 and non-
274             # validators a weight of 50 meaning non-validators normally go first), then
275             # alphabetical
276             # TODO: These should probably sort in a consistent way independent of locale
277             return [
278 29 50       1001 sort { ( $a->weight <=> $b->weight ) || ( $a->name cmp $b->name ) }
279 80         4329 map { $self->_load_plugin( $_, $self->_plugins_to_run->{$_} ) }
280 66         150 keys %{ $self->_plugins_to_run }
  66         1260  
281             ];
282             }
283              
284             sub _load_plugin {
285 80     80   788 my ( $self, $plugin_name, $plugin_conf ) = @_;
286              
287             # Extract first name in case there is a description
288             #
289 80         322 my ($plugin_fname) = ( $plugin_name =~ /^(\S+)/ );
290              
291 80 100       437 my $plugin_class = (
292             $plugin_fname =~ /^\+/
293             ? substr( $plugin_fname, 1 )
294             : "Code::TidyAll::Plugin::$plugin_fname"
295             );
296             try {
297 80 50   80   4492 use_module($plugin_class) || die 'not found';
298             }
299             catch {
300 1     1   325 die qq{could not load plugin class '$plugin_class': $_};
301 80         813 };
302              
303 79         27720 return $plugin_class->new(
304             name => $plugin_name,
305             tidyall => $self,
306             %$plugin_conf
307             );
308             }
309              
310             sub BUILD {
311 70     70 0 5510 my ( $self, $params ) = @_;
312              
313             # Strict constructor
314             #
315 70 100       291 if ( my @bad_params = grep { !$self->can($_) } keys(%$params) ) {
  208         1060  
316             die sprintf(
317             'unknown constructor param%s %s for %s',
318             @bad_params > 1 ? 's' : q{},
319 1 50       28 join( ', ', sort map {qq['$_']} @bad_params ),
  2         37  
320             ref($self)
321             );
322             }
323              
324 69 100       411 unless ( $self->no_backups ) {
325 65         1485 $self->_backup_dir->mkpath( { mode => 0775 } );
326 65         40000 $self->_purge_backups_periodically();
327             }
328              
329 69         2590 @INC = ( @{ $self->inc }, @INC );
  69         1331  
330             }
331              
332             sub _purge_backups_periodically {
333 65     65   192 my ($self) = @_;
334 65         1757 my $cache = $self->cache;
335 65   100     32549 my $last_purge_backups = $cache->get('last_purge_backups') || 0;
336 65 100       6716 if ( time > $last_purge_backups + $self->_backup_ttl_secs ) {
337 42         4315 $self->_purge_backups();
338 42         5650 $cache->set( 'last_purge_backups', time() );
339             }
340             }
341              
342             sub _purge_backups {
343 42     42   123 my ($self) = @_;
344 42 100       202 $self->msg('purging old backups') if $self->verbose;
345             find(
346             {
347             follow => 0,
348             wanted => sub {
349 42 0 33 42   5515 unlink $_ if -f && /\.bak$/ && time > ( stat($_) )[9] + $self->_backup_ttl_secs;
      33        
350             },
351 42         1175 no_chdir => 1
352             },
353             $self->_backup_dir,
354             );
355             }
356              
357             sub new_from_conf_file {
358 3     3 1 33733 my ( $class, $conf_file, %params ) = @_;
359              
360 3         13 $conf_file = path($conf_file);
361              
362 3 50       58 die qq{no such file '$conf_file'} unless $conf_file->is_file;
363 3         70 my $conf_params = $class->_read_conf_file($conf_file);
364 3   100     19 my $main_params = delete( $conf_params->{'_'} ) || {};
365              
366             %params = (
367             plugins => $conf_params,
368             root_dir => path($conf_file)->realpath->parent,
369 3         13 %{$main_params},
  3         731  
370             %params
371             );
372              
373             # Initialize with alternate class if given
374             #
375 3 50       19 if ( my $tidyall_class = delete( $params{tidyall_class} ) ) {
376 0 0       0 local @INC = ( @{ $conf_params->{inc} }, @INC ) if $conf_params->{inc};
  0         0  
377 0 0       0 use_module($tidyall_class) or die qq{cannot load '$tidyall_class'};
378 0         0 $class = $tidyall_class;
379             }
380              
381 3 50       12 if ( $params{verbose} ) {
382 0   0     0 my $msg_outputter = $params{msg_outputter} || $class->_build_msg_outputter();
383 0         0 $msg_outputter->(
384             'constructing %s with these params: %s', $class,
385             _dump_params( \%params )
386             );
387             }
388              
389 3         63 return $class->new(%params);
390             }
391              
392             sub _read_conf_file {
393 3     3   11 my ( $class, $conf_file ) = @_;
394 3         15 my $conf_string = $conf_file->slurp_utf8;
395 3         1406 my $root_dir = $conf_file->parent;
396 3         233 $conf_string =~ s/\$ROOT/$root_dir/g;
397 3         41 my $conf_hash = Code::TidyAll::Config::INI::Reader->read_string($conf_string);
398 3 50       216 die qq{'$conf_file' did not evaluate to a hash}
399             unless ( ref($conf_hash) eq 'HASH' );
400 3         11 return $conf_hash;
401             }
402              
403             sub _dump_params {
404 0     0   0 my $p = shift;
405              
406 0         0 return Data::Dumper->new( [ _recurse_dump($p) ] )->Indent(0)->Sortkeys(1)->Quotekeys(0)
407             ->Terse(1)->Dump;
408             }
409              
410             # This is all a ridiculous workaround around the fact that there is no good
411             # way to tell Data::Dumper how to serialize a Path::Tiny object.
412             sub _recurse_dump {
413 0     0   0 my ($p) = @_;
414              
415 0 0       0 return $p unless ref $p;
416              
417 0 0       0 if ( ref $p eq 'HASH' ) {
    0          
418 0         0 my %dump;
419 0         0 for my $k ( keys %{$p} ) {
  0         0  
420 0         0 my $v = $p->{$k};
421 0 0       0 if ( blessed $v ) {
    0          
422 0 0       0 if ( $v->isa('Path::Tiny') ) {
423 0         0 $dump{$k} = $v . q{};
424             }
425             else {
426 0         0 die 'Cannot dump ' . ref($v) . ' object';
427             }
428             }
429             elsif ( ref $v =~ /^(?:HASH|ARRAY)$/ ) {
430 0         0 $dump{$k} = _recurse_dump($v);
431             }
432             else {
433 0         0 $dump{$k} = $v;
434             }
435             }
436 0         0 return \%dump;
437             }
438             elsif ( ref $p eq 'ARRAY' ) {
439 0         0 my @dump;
440 0         0 for my $v ( @{$p} ) {
  0         0  
441 0 0       0 if ( blessed $v ) {
    0          
442 0 0       0 if ( $v->isa('Path::Tiny') ) {
443 0         0 push @dump, $v . q{};
444             }
445             else {
446 0         0 die 'Cannot dump ' . ref($v) . ' object';
447             }
448             }
449             elsif ( ref $v =~ /^(?:HASH|ARRAY)$/ ) {
450 0         0 push @dump, _recurse_dump($v);
451             }
452             else {
453 0         0 push @dump, $v;
454             }
455             }
456 0         0 return \@dump;
457             }
458              
459 0         0 die "_recurse_dump was called with a value that was not a scalar, hashref, or an arrayref: $p";
460             }
461              
462             sub process_all {
463 17     17 0 15553 my $self = shift;
464              
465 17         87 return $self->process_paths( $self->find_matched_files );
466             }
467              
468             sub process_paths {
469 64     64 1 71766 my ( $self, @paths ) = @_;
470              
471             @paths = map {
472 85     85   2424 try { $_->realpath }
473 85 100       6755 || $_->absolute
474 64         205 } map { path($_) } @paths;
  85         549  
475              
476 64         12732 my $dir = pushd( $self->root_dir );
477 64 100 66     7364 if ( $self->jobs > 1 && @paths > 1 ) {
478 5         25 return $self->_process_parallel(@paths);
479             }
480             else {
481 59         142 return map { $self->process_path($_) } @paths;
  65         204  
482             }
483             }
484              
485             sub _process_parallel {
486 5     5   20 my ( $self, @paths ) = @_;
487              
488 5 50       10 unless ( eval { require Parallel::ForkManager; 1; } ) {
  5         40  
  5         25  
489 0         0 die 'Running Code::TidyAll with multiple jobs requires Parallel::ForkManager';
490             }
491              
492 5         10 my @results;
493             my %path_to_pid;
494              
495 5         175 my $pm = Parallel::ForkManager->new( $self->jobs );
496 5         21450 $pm->set_waitpid_blocking_sleep(0.01);
497             $pm->run_on_finish(
498             sub {
499 5     5   2783575 my ( $pid, $code, $result ) = @_[ 0, 1, 5 ];
500              
501 5 50       54 if ($code) {
502 0         0 warn "Error running tidyall on $path_to_pid{$pid}. Got exit status of $code.";
503             }
504             else {
505 5         69 push @results, $result;
506             }
507             }
508 5         65 );
509              
510 5         60 for my $path (@paths) {
511 14 100       144 if ( my $pid = $pm->start ) {
512 10         14262 $path_to_pid{$path} = $pid;
513 10         683 next;
514             }
515              
516 4         14087 $pm->finish( 0, $self->process_path($path) );
517             }
518              
519 1         70 $pm->wait_all_children;
520              
521 1         56 return @results;
522             }
523              
524             sub process_path {
525 69     69 0 262 my ( $self, $path ) = @_;
526              
527 69 100       432 if ( $path->is_dir ) {
    100          
528 2 100       56 if ( $self->recursive ) {
529 1         25 return $self->process_paths( $path->children );
530             }
531             else {
532 1         11 return ( $self->_error_result( "$path: is a directory (try -r/--recursive)", $path ) );
533             }
534             }
535             elsif ( $path->is_file ) {
536 66         2459 return ( $self->process_file($path) );
537             }
538             else {
539 1         41 return ( $self->_error_result( "$path: not a file or directory", $path ) );
540             }
541             }
542              
543             sub process_file {
544 68     68 1 3102 my ( $self, $full_path ) = @_;
545              
546 68         331 $full_path = path($full_path);
547 68 50       1495 die "$full_path is not a file" unless $full_path->is_file;
548              
549 68         1289 my $path = $self->_small_path($full_path);
550              
551 67 50       3607 if ( $self->list_only ) {
552 0 0       0 if ( my @plugins = $self->plugins_for_path($path) ) {
553 0         0 $self->msg( '%s (%s)', $path, join( ', ', map { $_->name } @plugins ) );
  0         0  
554             }
555 0         0 return Code::TidyAll::Result->new( path => $path, state => 'checked' );
556             }
557              
558 67         230 my $cache_model = $self->_cache_model_for( $path, $full_path );
559 67 50       74678 if ( $self->refresh_cache ) {
    100          
560 0         0 $cache_model->remove;
561             }
562             elsif ( $cache_model->is_cached ) {
563 4 50       310 $self->msg( '[cached] %s', $path ) if $self->verbose;
564 4         86 return Code::TidyAll::Result->new( path => $path, state => 'cached' );
565             }
566              
567 63   33     3293 my $contents = $cache_model->file_contents || $full_path->slurp_raw;
568 63         14450 my $result = $self->process_source( $contents, $path );
569              
570 63 100       34837 if ( $result->state eq 'tidied' ) {
571              
572             # backup original contents
573 40         187 $self->_backup_file( $path, $contents );
574              
575             # write new contents out to disk
576 40         19654 $contents = $result->new_contents;
577              
578             # We don't use ->spew because that creates a new file and renames it,
579             # losing the existing mode setting in the process.
580 40         200 path( $full_path . $self->output_suffix )->append_raw( { truncate => 1 }, $contents );
581              
582             # change the in memory contents of the cache (but don't update yet)
583 40 50       15529 $cache_model->file_contents($contents) unless $self->output_suffix;
584             }
585              
586 63 100       304 $cache_model->update if $result->ok;
587 63         732 return $result;
588             }
589              
590             sub _small_path {
591 68     68   216 my ( $self, $path ) = @_;
592 68 100       331 die sprintf( q{'%s' is not underneath root dir '%s'!}, $path, $self->root_dir )
593             unless index( $path, $self->root_dir ) == 0;
594 67         846 return path( substr( $path . q{}, length( $self->root_dir ) + 1 ) );
595             }
596              
597             sub plugins_for_path {
598 85     85 1 6088 my ( $self, $path ) = @_;
599              
600             $self->_plugins_for_path->{$path}
601 85   100     2091 ||= [ grep { $_->matches_path($path) } @{ $self->_plugin_objects } ];
  49         1631  
  44         3281  
602 85         2158 return @{ $self->_plugins_for_path->{$path} };
  85         1452  
603             }
604              
605             sub _cache_model_for {
606 67     67   200 my ( $self, $path, $full_path ) = @_;
607 67 100       2622 return $self->cache_model_class->new(
608             path => $path,
609             full_path => $full_path,
610             ( $self->no_cache ? () : ( cache_engine => $self->cache ) ),
611             base_sig => $self->_base_sig,
612             );
613             }
614              
615             sub _backup_file {
616 40     40   136 my ( $self, $path, $contents ) = @_;
617 40 100       175 unless ( $self->no_backups ) {
618 32         830 my $backup_file = $self->_backup_dir->child( $self->_backup_filename($path) );
619 32         7418 $backup_file->parent->mkpath( { mode => 0775 } );
620 32         5624 $backup_file->spew_raw($contents);
621             }
622             }
623              
624             sub _backup_filename {
625 32     32   488 my ( $self, $path ) = @_;
626              
627 32         301 return join( q{}, $path, '-', time2str( '%Y%m%d-%H%M%S', time ), '.bak' );
628             }
629              
630             sub process_source {
631 84     84 1 27903 my ( $self, $contents, $path ) = @_;
632              
633 84         296 $path = path($path);
634              
635 84 50 33     2335 die 'contents and path required' unless defined($contents) && defined($path);
636 84         379 my @plugins = $self->plugins_for_path($path);
637              
638 84 100       1182 if ( !@plugins ) {
639 1 0       34 $self->msg(
    50          
640             '[no plugins apply%s] %s',
641             $self->mode ? q{ for mode '} . $self->mode . q{'} : q{}, $path
642             ) if $self->verbose;
643 1         35 return Code::TidyAll::Result->new( path => $path, state => 'no_match' );
644             }
645              
646 83 100       359 if ( $self->verbose ) {
647 4         15 my @names = join ', ', map { $_->name } @plugins;
  13         52  
648 4         26 $self->msg("[applying the following plugins: @names]");
649             }
650              
651 83         212 my $new_contents = my $orig_contents = $contents;
652 83         241 my $plugin;
653             my $error;
654 83         0 my @diffs;
655             try {
656 83     83   4301 foreach my $method (qw(preprocess_source process_source_or_file postprocess_source)) {
657 236         446 foreach $plugin (@plugins) {
658 282         391 my $diff;
659 282         1737 ( $new_contents, $diff )
660             = $plugin->$method( $new_contents, $path, $self->check_only );
661 269 100       977 if ($diff) {
662 2         13 push @diffs, [ $plugin->name, $diff ];
663             }
664             }
665             }
666             }
667             catch {
668 13     13   1158 chomp;
669 13         48 $error = $_;
670 13 50       169 $error = sprintf( q{*** '%s': %s}, $plugin->name, $_ ) if $plugin;
671 83         1019 };
672              
673 83   100     2502 my $was_tidied = !$error && ( $new_contents ne $orig_contents );
674 83 100 100     507 if ( $was_tidied && $self->check_only ) {
675 5         21 $error = '*** needs tidying';
676              
677             # Github annotations parsable output to highlight code in pull requests
678 5 100       30 if ( $ENV{GITHUB_ACTIONS} ) {
679 1         5 $error .= "\n::error file=${path}::File ${path} needs tidying";
680             }
681              
682 5         29 foreach my $diff (@diffs) {
683 2         4 $error .= "\n\n";
684 2         9 $error .= "$diff->[0] made the following change:\n$diff->[1]";
685             }
686 5 100       35 $error .= "\n\n" if @diffs;
687 5         10 undef $was_tidied;
688             }
689              
690 83 100 100     531 if ( !$self->quiet || $error ) {
691 67 100       259 my $status = $was_tidied ? '[tidied] ' : '[checked] ';
692             my $plugin_names
693 67 100       346 = $self->verbose ? sprintf( ' (%s)', join( ', ', map { $_->name } @plugins ) ) : q{};
  13         55  
694 67         324 $self->msg( '%s%s%s', $status, $path, $plugin_names );
695             }
696              
697 83 100       3395 if ($error) {
    100          
698 18         175 return $self->_error_result( $error, $path, $orig_contents, $new_contents );
699             }
700             elsif ($was_tidied) {
701 46         1637 return Code::TidyAll::Result->new(
702             path => $path,
703             state => 'tidied',
704             orig_contents => $orig_contents,
705             new_contents => $new_contents
706             );
707             }
708             else {
709 19         669 return Code::TidyAll::Result->new( path => $path, state => 'checked' );
710             }
711             }
712              
713             sub _error_result {
714 20     20   128 my ( $self, $msg, $path, $orig_contents, $new_contents ) = @_;
715 20         89 $self->msg( '%s', $msg );
716 20 100 66     1449 return Code::TidyAll::Result->new(
    100 66        
717             path => $path,
718             state => 'error',
719             error => $msg,
720             (
721             ( defined $orig_contents && length $orig_contents )
722             ? ( orig_contents => $orig_contents )
723             : ()
724             ),
725             (
726             ( defined $new_contents && length $new_contents )
727             ? ( new_contents => $new_contents )
728             : ()
729             ),
730             );
731             }
732              
733             sub find_conf_file {
734 0     0 1 0 my ( $class, $conf_names, $start_dir ) = @_;
735              
736 0         0 $start_dir = path($start_dir);
737 0         0 my $path1 = $start_dir->absolute;
738 0         0 my $path2 = $start_dir->realpath;
739 0   0     0 my $conf_file = $class->_find_conf_file_upward( $conf_names, $path1 )
740             || $class->_find_conf_file_upward( $conf_names, $path2 );
741 0 0       0 unless ( defined $conf_file ) {
742 0 0       0 die sprintf(
743             'could not find %s upwards from %s',
744             join( ' or ', @$conf_names ),
745             ( $path1 eq $path2 ) ? qq{'$path1'} : qq{'$path1' or '$path2'}
746             );
747             }
748 0         0 return $conf_file;
749             }
750              
751             sub _find_conf_file_upward {
752 0     0   0 my ( $class, $conf_names, $search_dir ) = @_;
753              
754 0         0 my $cnt = 0;
755 0         0 while (1) {
756 0         0 foreach my $conf_name (@$conf_names) {
757 0         0 my $try_path = $search_dir->child($conf_name);
758 0 0       0 return $try_path if $try_path->is_file;
759             }
760              
761 0         0 my $parent = $search_dir->parent;
762 0 0       0 last if $parent eq $search_dir;
763 0         0 $search_dir = $parent;
764              
765 0 0       0 die 'inf loop!' if ++$cnt > 100;
766             }
767             }
768              
769             sub find_matched_files {
770 19     19 1 61 my ($self) = @_;
771              
772 19         575 my $plugins_for_path = $self->_plugins_for_path;
773 19         939 my $root_length = length( $self->root_dir );
774              
775 19         404 my @all;
776 19         54 for my $plugin ( @{ $self->_plugin_objects } ) {
  19         409  
777 30         1105 my @matched = $self->_matched_by_plugin($plugin);
778 30         87 push @all, @matched;
779              
780             # When we end up in process_source we'll need to know which plugins
781             # match a given file. This could be (re-)calculated When we call
782             # ->plugins_for_path($file) there but since we already know the path
783             # to plugin mapping, we might as well store it here.
784 30         73 for my $file (@matched) {
785 54         124 my $path = substr( $file, $root_length + 1 );
786 54   100     244 $plugins_for_path->{$path} ||= [];
787 54         68 push @{ $plugins_for_path->{$path} }, $plugin;
  54         160  
788             }
789             }
790              
791 19         244 return map { path($_) } uniq(@all);
  41         1021  
792             }
793              
794             sub _matched_by_plugin {
795 30     30   60 my $self = shift;
796 30         55 my $plugin = shift;
797              
798 7         28 my %is_ignored = map { $_ => 1 }
799 30 50       56 $self->_zglob( [ @{ $self->ignores || [] }, @{ $plugin->ignores || [] } ] );
  30 50       678  
  30         1393  
800             my @matched
801 30 100 100     712 = grep { !$is_ignored{$_} } grep { -f && -s && !-l } $self->_zglob( $plugin->selects );
  64         217  
  67         2015  
802              
803 30 100       325 my $shebang = $plugin->shebang
804             or return @matched;
805              
806 1         3 my $re = join '|', map {quotemeta} @{$shebang};
  2         8  
  1         3  
807 1         40 $re = qr/^#!.*\b(?:$re)\b/;
808             return grep {
809 1         5 my $fh;
  6         14  
810 6 50       180 open $fh, '<', $_ or die $!;
811 6         218 scalar <$fh> =~ /$re/;
812             } @matched;
813             }
814              
815             sub _zglob {
816 60     60   2497 my ( $self, $globs ) = @_;
817              
818 60         132 local $Code::TidyAll::Zglob::NOCASE = 0;
819 60         86 my @files;
820 60         131 foreach my $glob (@$globs) {
821             try {
822 43     43   2262 push @files, zglob( join( "/", $self->root_dir, $glob ) );
823             }
824             catch {
825 0     0   0 die qq{error parsing '$glob': $_};
826             }
827 43         422 }
828 60         1140 return uniq(@files);
829             }
830              
831             sub msg {
832 95     95 0 333 my ( $self, $format, @params ) = @_;
833 95         521 $self->msg_outputter()->( $format, @params );
834             }
835              
836             sub _build_msg_outputter {
837             return sub {
838 95     95   180 my $format = shift;
839 95         1182 printf "$format\n", @_;
840 70     70   11436 };
841             }
842              
843             1;
844              
845             # ABSTRACT: Engine for tidyall, your all-in-one code tidier and validator
846              
847             __END__
848              
849             =pod
850              
851             =encoding UTF-8
852              
853             =head1 NAME
854              
855             Code::TidyAll - Engine for tidyall, your all-in-one code tidier and validator
856              
857             =head1 VERSION
858              
859             version 0.83
860              
861             =head1 SYNOPSIS
862              
863             use Code::TidyAll;
864              
865             my $ct = Code::TidyAll->new_from_conf_file(
866             '/path/to/conf/file',
867             ...
868             );
869              
870             # or
871              
872             my $ct = Code::TidyAll->new(
873             root_dir => '/path/to/root',
874             plugins => {
875             perltidy => {
876             select => 'lib/**/*.(pl|pm)',
877             argv => '-noll -it=2',
878             },
879             ...
880             }
881             );
882              
883             # then...
884              
885             $ct->process_paths($file1, $file2);
886              
887             =head1 DESCRIPTION
888              
889             This is the engine used by L<tidyall> - read that first to get an overview.
890              
891             You can call this API from your own program instead of executing C<tidyall>.
892              
893             =head1 METHODS
894              
895             This class offers the following methods:
896              
897             =head2 Code::TidyAll->new(%params)
898              
899             The regular constructor. Must pass at least I<plugins> and I<root_dir>.
900              
901             =head2 $tidyall->new_from_conf_file( $conf_file, %params )
902              
903             Takes a conf file path, followed optionally by a set of key/value parameters.
904             Reads parameters out of the conf file and combines them with the passed
905             parameters (the latter take precedence), and calls the regular constructor.
906              
907             If the conf file or params defines I<tidyall_class>, then that class is
908             constructed instead of C<Code::TidyAll>.
909              
910             =head3 Constructor parameters
911              
912             =over 4
913              
914             =item * plugins
915              
916             Specify a hash of plugins, each of which is itself a hash of options. This is
917             equivalent to what would be parsed out of the sections in the configuration
918             file.
919              
920             =item * selected_plugins
921              
922             An arrayref of plugins to be used. This overrides the C<mode> parameter.
923              
924             This is really only useful if you're getting configuration from a config file
925             and want to narrow the set of plugins to be run.
926              
927             Note that plugins will still only run on files which match their C<select> and
928             C<ignore> configuration.
929              
930             =item * cache_model_class
931              
932             The cache model class. Defaults to C<Code::TidyAll::CacheModel>
933              
934             =item * cache
935              
936             The cache instance (e.g. an instance of C<Code::TidyAll::Cache> or a C<CHI>
937             instance.) An instance of C<Code::TidyAll::Cache> is automatically instantiated
938             by default.
939              
940             =item * backup_ttl
941              
942             =item * check_only
943              
944             If this is true, then we simply check that files pass validation steps and that
945             tidying them does not change the file. Any changes from tidying are not
946             actually written back to the file.
947              
948             =item * no_cleanup
949              
950             A boolean indicating if we should skip cleaning temporary files or not.
951             Defaults to false.
952              
953             =item * inc
954              
955             An arrayref of directories to prepend to C<@INC>. This can be set via the
956             command-line as C<-I>, but you can also set it in a config file.
957              
958             This affects both loading and running plugins.
959              
960             =item * data_dir
961              
962             =item * iterations
963              
964             =item * mode
965              
966             =item * no_backups
967              
968             =item * no_cache
969              
970             =item * output_suffix
971              
972             =item * quiet
973              
974             =item * root_dir
975              
976             =item * ignore
977              
978             =item * verbose
979              
980             These options are the same as the equivalent C<tidyall> command-line options,
981             replacing dashes with underscore (e.g. the C<backup-ttl> option becomes
982             C<backup_ttl> here).
983              
984             =item * msg_outputter
985              
986             This is a subroutine reference that is called whenever a message needs to be
987             printed in some way. The sub receives a C<sprintf()> format string followed by
988             one or more parameters. The default sub used simply calls C<printf "$format\n",
989             @_> but L<Test::Code::TidyAll> overrides this to use the C<<
990             Test::Builder->diag >> method.
991              
992             =back
993              
994             =head2 $tidyall->process_paths( $path, ... )
995              
996             This method iterates through a list of paths, processing all the files it
997             finds. It will descend into subdirectories if C<recursive> flag is true.
998             Returns a list of L<Code::TidyAll::Result> objects, one for each file.
999              
1000             =head2 $tidyall->process_file( $file )
1001              
1002             Process the one I<file>, meaning:
1003              
1004             =over 4
1005              
1006             =item *
1007              
1008             Check the cache and return immediately if file has not changed.
1009              
1010             =item *
1011              
1012             Apply appropriate matching plugins.
1013              
1014             =item *
1015              
1016             Print success or failure result to STDOUT, depending on quiet/verbose settings.
1017              
1018             =item *
1019              
1020             Write to the cache if caching is enabled.
1021              
1022             =item *
1023              
1024             Return a L<Code::TidyAll::Result> object.
1025              
1026             =back
1027              
1028             =head2 $tidyall->process_source( $source, $path )
1029              
1030             Like C<process_file>, but process the I<source> string instead of a file, and
1031             does not read from or write to the cache. You must still pass the relative
1032             I<path> from the root as the second argument, so that we know which plugins to
1033             apply. Returns a L<Code::TidyAll::Result> object.
1034              
1035             =head2 $tidyall->plugins_for_path($path)
1036              
1037             Given a relative I<path> from the root, returns a list of
1038             L<Code::TidyAll::Plugin> objects that apply to it, or an empty list if no
1039             plugins apply.
1040              
1041             =head2 $tidyall->find_matched_files
1042              
1043             Returns a list of sorted files that match at least one plugin in configuration.
1044              
1045             =head2 Code::TidyAll->find_conf_file( $conf_names, $start_dir )
1046              
1047             Start in the I<start_dir> and work upwards, looking for a file matching one of
1048             the I<conf_names>. Returns the pathname if found or throw an error if not
1049             found.
1050              
1051             =head1 SUPPORT
1052              
1053             Bugs may be submitted at L<https://github.com/houseabsolute/perl-code-tidyall/issues>.
1054              
1055             =head1 SOURCE
1056              
1057             The source code repository for Code-TidyAll can be found at L<https://github.com/houseabsolute/perl-code-tidyall>.
1058              
1059             =head1 AUTHORS
1060              
1061             =over 4
1062              
1063             =item *
1064              
1065             Jonathan Swartz <swartz@pobox.com>
1066              
1067             =item *
1068              
1069             Dave Rolsky <autarch@urth.org>
1070              
1071             =back
1072              
1073             =head1 CONTRIBUTORS
1074              
1075             =for stopwords Adam Herzog Andreas Vögele Andy Jack Bernhard Schmalhofer Finn Smith George Hartzell Graham Knop Gregory Oschwald Joe Crotty Kenneth Ölwing Mark Fowler Grimes Martin Gruner Mohammad S Anwar Nick Tonkin Olaf Alders Paulo Custodio Pavel Dostál Pedro Melo Ricardo Signes Sergey Romanov Shlomi Fish timgimyee
1076              
1077             =over 4
1078              
1079             =item *
1080              
1081             Adam Herzog <adam@adamherzog.com>
1082              
1083             =item *
1084              
1085             Andreas Vögele <andreas@andreasvoegele.com>
1086              
1087             =item *
1088              
1089             Andy Jack <andyjack@cpan.org>
1090              
1091             =item *
1092              
1093             Bernhard Schmalhofer <Bernhard.Schmalhofer@gmx.de>
1094              
1095             =item *
1096              
1097             Finn Smith <finn@timeghost.net>
1098              
1099             =item *
1100              
1101             George Hartzell <georgewh@gene.com>
1102              
1103             =item *
1104              
1105             Graham Knop <haarg@haarg.org>
1106              
1107             =item *
1108              
1109             Gregory Oschwald <goschwald@maxmind.com>
1110              
1111             =item *
1112              
1113             Joe Crotty <joe.crotty@returnpath.net>
1114              
1115             =item *
1116              
1117             Kenneth Ölwing <kenneth.olwing@skatteverket.se>
1118              
1119             =item *
1120              
1121             Mark Fowler <mark@twoshortplanks.com>
1122              
1123             =item *
1124              
1125             Mark Grimes <mgrimes@cpan.org>
1126              
1127             =item *
1128              
1129             Martin Gruner <martin.gruner@otrs.com>
1130              
1131             =item *
1132              
1133             Mohammad S Anwar <mohammad.anwar@yahoo.com>
1134              
1135             =item *
1136              
1137             Nick Tonkin <ntonkin@bur-ntonkin-m1.corp.endurance.com>
1138              
1139             =item *
1140              
1141             Olaf Alders <olaf@wundersolutions.com>
1142              
1143             =item *
1144              
1145             Paulo Custodio <pauloscustodio@gmail.com>
1146              
1147             =item *
1148              
1149             Pavel Dostál <pdostal@suse.cz>
1150              
1151             =item *
1152              
1153             Pedro Melo <melo@simplicidade.org>
1154              
1155             =item *
1156              
1157             Ricardo Signes <rjbs@cpan.org>
1158              
1159             =item *
1160              
1161             Sergey Romanov <sromanov-dev@yandex.ru>
1162              
1163             =item *
1164              
1165             Shlomi Fish <shlomif@shlomifish.org>
1166              
1167             =item *
1168              
1169             timgimyee <tim.gim.yee@gmail.com>
1170              
1171             =back
1172              
1173             =head1 COPYRIGHT AND LICENSE
1174              
1175             This software is copyright (c) 2011 - 2022 by Jonathan Swartz.
1176              
1177             This is free software; you can redistribute it and/or modify it under
1178             the same terms as the Perl 5 programming language system itself.
1179              
1180             The full text of the license can be found in the
1181             F<LICENSE> file included with this distribution.
1182              
1183             =cut