File Coverage

blib/lib/Module/CPANTS/Kwalitee/Files.pm
Criterion Covered Total %
statement 134 217 61.7
branch 58 122 47.5
condition 19 54 35.1
subroutine 23 36 63.8
pod 3 3 100.0
total 237 432 54.8


line stmt bran cond sub pod time code
1             package Module::CPANTS::Kwalitee::Files;
2 7     7   4160 use warnings;
  7         22  
  7         231  
3 7     7   44 use strict;
  7         34  
  7         152  
4 7     7   3889 use File::Find::Object;
  7         83195  
  7         242  
5 7     7   65 use File::Spec::Functions qw(catfile);
  7         18  
  7         370  
6 7     7   39 use File::stat;
  7         25  
  7         79  
7 7     7   4695 use ExtUtils::Manifest qw(maniskip);
  7         47597  
  7         20429  
8             $ExtUtils::Manifest::Quiet = 1;
9              
10             our $VERSION = '1.02';
11             $VERSION =~ s/_//; ## no critic
12              
13             our $RespectManiskip = 1; # for Test::Kwalitee and its friends
14              
15 35     35 1 85 sub order { 15 }
16              
17             ##################################################################
18             # Analyse
19             ##################################################################
20              
21             sub analyse {
22 12     12 1 55 my $class = shift;
23 12         30 my $me = shift;
24 12         278 my $distdir = $me->distdir;
25 12 50       143 $distdir =~ s|\\|/|g if $^O eq 'MSWin32';
26              
27             # Respect no_index if possible
28 12         101 my $no_index_re = $class->_make_no_index_regex($me);
29 12         127 my $maniskip = $class->_make_maniskip($me, $distdir);
30              
31 12         82 my (%files, %dirs);
32 12         0 my (@files_array, @dirs_array, @files_to_be_skipped);
33 12         37 my $size = 0;
34 12         66 my $latest_mtime = 0;
35 12         46 my @base_dirs;
36 12         356 my $finder = File::Find::Object->new({
37             depth => 1,
38             followlink => 0,
39             }, $distdir);
40 12         4733 my %seen; # GH-83
41 12         199 while(defined(my $name = $finder->next)) {
42 57 50       24254 $name =~ s|\\|/|g if $^O eq 'MSWin32';
43 57 50       894 (my $path = $name) =~ s!^\Q$distdir\E(?:/|$)!! or next;
44 57 50       184 next if $path eq '';
45 57 100       279 next if $seen{$path}++;
46              
47 45 100       1155 if ($me->d->{is_local_distribution}) {
48 43 50       424 next if $path =~ m!/\.!;
49             }
50              
51 45 50 33     132 if ($maniskip && $maniskip->($path)) {
52 0 0       0 next if $RespectManiskip;
53 0         0 push @files_to_be_skipped, $path;
54 0 0       0 if (-d $name) { $dirs{$path}{maniskip} = 1 }
  0         0  
55 0         0 else { $files{$path}{maniskip} = 1 }
56             }
57              
58 45 100       684 if (-d $name) {
59 25   50     255 $dirs{$path} ||= {};
60 25 50       268 if (-l $name) {
61 0         0 $dirs{$path}{symlink} = 1;
62             }
63 25         100 push @dirs_array, $path;
64 25         132 next;
65             }
66              
67 20 50       287 if (my $stat = stat($name)) {
68 20   50     4504 $files{$path}{size} = $stat->size || 0;
69 20         273 $size += $files{$path}{size};
70              
71 20         345 my $mtime = $files{$path}{mtime} = $stat->mtime;
72 20 100       253 $latest_mtime = $mtime if $mtime > $latest_mtime;
73             } else {
74 0         0 $files{$path}{stat_error} = $!;
75 0         0 next;
76             }
77              
78 20 100       322 if (-l $name) {
79 2         39 $files{$path}{symlink} = 1;
80             }
81              
82 20 50 33     98 if ($no_index_re && $path =~ qr/$no_index_re/) {
83 0         0 $files{$path}{no_index} = 1;
84 0         0 next;
85             }
86              
87 20 50       262 if (!-r $name) {
88 0         0 $files{$path}{unreadable} = 1;
89 0         0 next;
90             }
91              
92             # ignore files in dot directories (probably VCS stuff)
93 20 50       157 next if $path =~ m!(?:^|/)\.[^/]+/!;
94              
95 20         65 push @files_array, $path;
96              
97             # distribution may have several Makefile.PLs, thus
98             # several 'lib' or 't' directories to care
99 20 50 33     247 if ($path =~ m!/Makefile\.PL$! && $path !~ m!(^|/)x?t/!) {
100 0         0 (my $dir = $path) =~ s|/[^/]+$||;
101 0         0 push @base_dirs, $dir;
102             }
103             }
104              
105 12         953 $me->d->{size_unpacked} = $size;
106 12         375 $me->d->{latest_mtime} = $latest_mtime;
107              
108 0         0 my @symlinks = sort {$a cmp $b} (
109 20         93 grep({ $files{$_}{symlink} } keys %files),
110 12         126 grep({ $dirs{$_}{symlink} } keys %dirs)
  25         53  
111             );
112              
113 12 100       49 if (@symlinks) {
114 2         73 $me->d->{error}{symlinks} = join ',', @symlinks;
115             }
116              
117 12 50       95 if (@files_to_be_skipped) {
118 0         0 $me->d->{error}{no_files_to_be_skipped} = join ',', @files_to_be_skipped;
119             }
120              
121 12 50       33 $me->d->{base_dirs} = [sort @base_dirs] if @base_dirs;
122 12         62 my $base_dirs_re = join '|', '', map {quotemeta "$_/"} @base_dirs;
  0         0  
123              
124             # find special files/dirs
125 12         284 my @special_files = sort (qw(Makefile.PL Build.PL META.yml META.json MYMETA.yml MYMETA.json dist.ini cpanfile SIGNATURE MANIFEST MANIFEST.SKIP test.pl LICENSE LICENCE));
126 12         83 my @special_dirs = sort (qw(lib t xt));
127              
128 12         562 my %special_files_re = (
129             file_changelog => qr{^(?:$base_dirs_re)(?:chang|history)}i,
130             file_readme => qr{^(?:$base_dirs_re)readme(?:\.(?:txt|md|pod|mkdn|mdown|markdown))?}i,
131             );
132              
133 12         134 for my $base_dir ('', @base_dirs) {
134 12 50       57 $base_dir = "$base_dir/" if $base_dir;
135 12         32 for my $name (@special_files) {
136 168         506 my $file = "$base_dir$name";
137 168 100       374 if (exists $files{$file}) {
138 7         35 (my $key = "file_".lc $name) =~ s/\./_/;
139 7 50       192 $me->d->{$key} = $me->d->{$key} ? $me->d->{$key}.",$file" : $file;
140             }
141             }
142 12         44 for my $name (@special_dirs) {
143 36         251 my $dir = "$base_dir$name";
144 36 100       107 if (exists $dirs{$dir}) {
145 6         16 my $key = "dir_$name";
146 6 50       143 $me->d->{$key} = $me->d->{$key} ? $me->d->{$key}.",$dir" : $dir;
147             }
148             }
149             }
150              
151 12         61 for my $file (sort keys %files) {
152 20 100       248 next unless $file =~ m!^(?:$base_dirs_re)[^/]+$!;
153 13         70 while(my ($key, $re) = each %special_files_re) {
154 26 50       192 if ($file =~ /$re/) {
155 0 0       0 $me->d->{$key} = $me->d->{$key} ? $me->d->{$key}.",$file" : $file;
156             }
157             }
158             }
159              
160             # store stuff
161 12         278 $me->d->{files} = scalar @files_array;
162 12         400 $me->d->{files_array} = \@files_array;
163 12         310 $me->d->{files_hash} = \%files;
164 12         396 $me->d->{dirs} = scalar @dirs_array;
165 12         381 $me->d->{dirs_array} = \@dirs_array;
166              
167 12         106 my @ignored = grep {$files{$_}{no_index}} sort keys %files;
  20         71  
168 12 50       58 $me->d->{ignored_files_array} = \@ignored if @ignored;
169              
170             # check STDIN in Makefile.PL and Build.PL
171             # objective: convince people to use prompt();
172 12         36 for my $type (qw/makefile_pl build_pl/) {
173 24   50     716 for my $path (split ',', $me->d->{"file_$type"} || '') {
174 0 0       0 next unless $path;
175 0         0 my $file = catfile($me->distdir, $path);
176 0 0       0 next if not -e $file;
177 0 0       0 open my $fh, '<', $file or next;
178 0 0       0 my $content = do { local $/; <$fh> } or next;
  0         0  
  0         0  
179 0 0       0 $me->d->{"stdin_in_$type"} = 1 if $content =~ //;
180             }
181             }
182              
183 12         425 return;
184             }
185              
186             sub _make_no_index_regex {
187 12     12   46 my ($class, $me) = @_;
188              
189 12         306 my $meta = $me->d->{meta_yml};
190 12 100 66     150 return unless $meta && ref $meta eq ref {};
191              
192 3   33     23 my $no_index = $meta->{no_index} || $meta->{private};
193 3 50 33     36 return unless $no_index && ref $no_index eq ref {};
194              
195 0         0 my %map = (
196             file => '\z',
197             directory => '/',
198             );
199 0         0 my @ignore;
200 0         0 for my $type (qw/file directory/) {
201 0 0       0 next unless $no_index->{$type};
202 0         0 my $rest = $map{$type};
203             my @entries = ref $no_index->{$type} eq ref []
204 0         0 ? @{ $no_index->{$type} }
205 0 0       0 : ( $no_index->{$type} );
206             # entries may possibly have escape chars; DAGOLDEN/Class-InsideOut-0.90_01.tar.gz
207 0         0 push @ignore, map {s/\\/\\\\/g; "^$_$rest"} @entries;
  0         0  
  0         0  
208             }
209 0 0       0 return unless @ignore;
210              
211 0         0 $me->d->{no_index} = join ';', sort @ignore;
212 0         0 return '(?:' . (join '|', @ignore) . ')';
213             }
214              
215             sub _make_maniskip {
216 12     12   59 my ($class, $me, $distdir) = @_;
217              
218 12         83 my $maniskip_file = "$distdir/MANIFEST.SKIP";
219 12 50 33     269 return unless -f $maniskip_file && -r _;
220              
221             # ignore MANIFEST.SKIP if it has an invalid entry
222 0         0 my $maniskip_bak_file = "$maniskip_file.bak";
223 0         0 my $has_maniskip_bak = -f $maniskip_bak_file;
224              
225 0         0 my $maniskip = maniskip($maniskip_file);
226              
227 0         0 my $maniskip_warning;
228 0     0   0 local $SIG{__WARN__} = sub { $maniskip_warning = shift; };
  0         0  
229 0         0 eval { $maniskip->(""); };
  0         0  
230 0 0 0     0 if ($@ or $maniskip_warning) {
231 0   0     0 $me->d->{error}{no_maniskip_error} = $@ || $maniskip_warning;
232 0         0 $maniskip = undef;
233             }
234 0 0 0     0 if (-f $maniskip_bak_file && !$has_maniskip_bak) {
235 0         0 my $mtime = stat($maniskip_bak_file)->mtime;
236 0         0 utime $mtime, $mtime, $maniskip_file;
237              
238 0         0 unlink $maniskip_bak_file; # probably generated by #include_default
239             }
240 0         0 $maniskip;
241             }
242              
243             ##################################################################
244             # Kwalitee Indicators
245             ##################################################################
246              
247             sub kwalitee_indicators {
248             return [
249             {
250             name => 'has_readme',
251             error => q{The file "README" is missing from this distribution. The README provides some basic information to users prior to downloading and unpacking the distribution.},
252             remedy => q{Add a README to the distribution. It should contain a quick description of your module and how to install it.},
253 12 50   12   108 code => sub { shift->{file_readme} ? 1 : 0 },
254             details => sub {
255 0     0   0 my $d = shift;
256 0         0 return "README was not found.";
257             },
258             },
259             {
260             name => 'has_manifest',
261             error => q{The file "MANIFEST" is missing from this distribution. The MANIFEST lists all files included in the distribution.},
262             remedy => q{Add a MANIFEST to the distribution. Your buildtool should be able to autogenerate it (eg "make manifest" or "./Build manifest")},
263 12 100   12   113 code => sub { shift->{file_manifest} ? 1 : 0 },
264             details => sub {
265 0     0   0 my $d = shift;
266 0         0 return "MANIFEST was not found.";
267             },
268             },
269             {
270             name => 'has_meta_yml',
271             error => q{The file "META.yml" is missing from this distribution. META.yml is needed by people maintaining module collections (like CPAN), for people writing installation tools, or just people who want to know some stuff about a distribution before downloading it.},
272             remedy => q{Add a META.yml to the distribution. Your buildtool should be able to autogenerate it.},
273             code => sub {
274 12     12   71 my $d = shift;
275 12 100       41 return 1 if $d->{file_meta_yml};
276 9 50 66     77 return 1 if $d->{is_local_distribution} && $d->{file_mymeta_yml};
277 9         29 return 0;
278             },
279             details => sub {
280 0     0   0 my $d = shift;
281 0         0 return "META.yml was not found.";
282             },
283             },
284             {
285             name => 'has_meta_json',
286             error => q{The file "META.json" is missing from this distribution. META.json has better information than META.yml and is preferred by people maintaining module collections (like CPAN), for people writing installation tools, or just people who want to know some stuff about a distribution before downloading it.},
287             remedy => q{Add a META.json to the distribution. Your buildtool should be able to autogenerate it.},
288             code => sub {
289 12     12   93 my $d = shift;
290 12 50       38 return 1 if $d->{file_meta_json};
291 12 50 66     93 return 1 if $d->{is_local_distribution} && $d->{file_mymeta_json};
292 12         40 return 0;
293             },
294             details => sub {
295 0     0   0 my $d = shift;
296 0         0 return "META.json was not found.";
297             },
298             is_extra => 1,
299             },
300             {
301             name => 'has_buildtool',
302             error => q{Makefile.PL and/or Build.PL are missing. This makes installing this distribution hard for humans and impossible for automated tools like CPAN/CPANPLUS/cpanminus.},
303             remedy => q{Add a Makefile.PL (for ExtUtils::MakeMaker/Module::Install) or a Build.PL (for Module::Build and its friends), or use a distribution builder such as Dist::Zilla, Dist::Milla, Minilla.},
304             code => sub {
305 12     12   70 my $d = shift;
306 12 50 33     126 return 1 if $d->{file_makefile_pl} || $d->{file_build_pl};
307 12         37 return 0;
308             },
309             details => sub {
310 0     0   0 my $d = shift;
311 0         0 return "Neither Makefile.PL nor Build.PL was found.";
312             },
313             },
314             {
315             name => 'has_changelog',
316             error => q{The distribution hasn't got a Changelog (named something like m/^chang(es?|log)|history$/i). A Changelog helps people decide if they want to upgrade to a new version.},
317             remedy => q{Add a Changelog (best named 'Changes') to the distribution. It should list at least major changes implemented in newer versions.},
318 12 50   12   79 code => sub { shift->{file_changelog} ? 1 : 0 },
319             details => sub {
320 0     0   0 my $d = shift;
321 0         0 return "Any Changelog file was not found.";
322             },
323             },
324             {
325             name => 'no_files_to_be_skipped',
326             error => q{This distribution contains files that should be skipped by MANIFEST.SKIP.},
327             remedy => q{Fix MANIFEST.SKIP or use an authoring tool which respects MANIFEST.SKIP. Note that each entry in MANIFEST.SKIP is a regular expression. You may need to add appropriate meta characters not to ignore necessary stuff.},
328 12 50   12   110 code => sub {shift->{error}{no_files_to_be_skipped} ? 0 : 1},
329             details => sub {
330 0     0   0 my $d = shift;
331 0         0 return "The following files were found: ".$d->{error}{no_files_to_be_skipped};
332             },
333             },
334             {
335             name => 'no_symlinks',
336             error => q{This distribution includes symbolic links (symlinks). This is bad, because there are operating systems that do not handle symlinks.},
337             remedy => q{Remove the symlinks from the distribution.},
338 12 100   12   103 code => sub {shift->{error}{symlinks} ? 0 : 1},
339             details => sub {
340 0     0   0 my $d = shift;
341 0         0 return "The following symlinks were found: ".$d->{error}{symlinks};
342             },
343             },
344             {
345             name => 'has_tests',
346             error => q{This distribution doesn't contain either a file called 'test.pl' or a directory called 't'. This indicates that it doesn't contain even the most basic test-suite. This is really BAD!},
347             remedy => q{Add tests!},
348             code => sub {
349 12     12   80 my $d = shift;
350             # TODO: make sure if .t files do exist in t/ directory.
351 12 50 33     111 return 1 if $d->{file_test_pl} || $d->{dir_t};
352 12         67 return 0;
353             },
354             details => sub {
355 0     0   0 my $d = shift;
356 0         0 return q{Neither "test.pl" nor "t/" directory was not found.};
357             },
358             },
359             {
360             name => 'has_tests_in_t_dir',
361             is_extra => 1,
362             error => q{This distribution contains either a file called 'test.pl' (the old test file) or is missing a directory called 't'. This indicates that it uses the old test mechanism or it has no test-suite.},
363             remedy => q{Add tests or move tests.pl to the t/ directory!},
364             code => sub {
365 12     12   98 my $d = shift;
366             # TODO: make sure if .t files do exist in t/ directory.
367 12 50 33     147 return 1 if !$d->{file_test_pl} && $d->{dir_t};
368 12         54 return 0;
369             },
370             details => sub {
371 0     0   0 my $d = shift;
372 0 0       0 return q{"test.pl" was found.} if $d->{file_test_pl};
373 0         0 return q{"t/" directory was not found.};
374             },
375             },
376             {
377             name => 'no_stdin_for_prompting',
378             error => q{This distribution is using direct call from STDIN instead of prompt(). Make sure STDIN is not used in Makefile.PL or Build.PL.},
379             is_extra => 1,
380             remedy => q{Use the prompt() method from ExtUtils::MakeMaker/Module::Build.},
381             code => sub {
382 12     12   84 my $d = shift;
383 12 50 33     132 if ($d->{stdin_in_makefile_pl}||$d->{stdin_in_build_pl}) {
384 0         0 return 0;
385             }
386 12         62 return 1;
387             },
388             details => sub {
389 0     0   0 my $d = shift;
390 0 0       0 return " was found in Makefile.PL" if $d->{stdin_in_makefile_pl};
391 0 0       0 return " was found in Build.PL" if $d->{stdin_in_build_pl};
392             },
393             },
394             {
395             name => 'no_maniskip_error',
396             error => q{This distribution's MANIFEST.SKIP has a problematic entry.},
397             is_extra => 1,
398             remedy => q{Fix the problematic entry.},
399             code => sub {
400 12     12   82 my $d = shift;
401 12 50       44 if ($d->{error}{no_maniskip_error}) {
402 0         0 return 0;
403             }
404 12         44 return 1;
405             },
406             details => sub {
407 0     0   0 my $d = shift;
408 0         0 return $d->{error}{no_maniskip_error};
409             },
410             },
411 8     8 1 665 ];
412             }
413              
414              
415             q{Favourite record of the moment:
416             Fat Freddys Drop: Based on a true story};
417              
418              
419             __END__