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   3466 use warnings;
  7         18  
  7         223  
3 7     7   34 use strict;
  7         14  
  7         122  
4 7     7   3137 use File::Find::Object;
  7         69225  
  7         199  
5 7     7   76 use File::Spec::Functions qw(catfile);
  7         18  
  7         379  
6 7     7   42 use File::stat;
  7         15  
  7         98  
7 7     7   3651 use ExtUtils::Manifest qw(maniskip);
  7         38402  
  7         16842  
8             $ExtUtils::Manifest::Quiet = 1;
9              
10             our $VERSION = '1.01';
11             $VERSION =~ s/_//; ## no critic
12              
13             our $RespectManiskip = 1; # for Test::Kwalitee and its friends
14              
15 35     35 1 78 sub order { 15 }
16              
17             ##################################################################
18             # Analyse
19             ##################################################################
20              
21             sub analyse {
22 11     11 1 53 my $class = shift;
23 11         22 my $me = shift;
24 11         201 my $distdir = $me->distdir;
25 11 50       95 $distdir =~ s|\\|/|g if $^O eq 'MSWin32';
26              
27             # Respect no_index if possible
28 11         82 my $no_index_re = $class->_make_no_index_regex($me);
29 11         48 my $maniskip = $class->_make_maniskip($me, $distdir);
30              
31 11         46 my (%files, %dirs);
32 11         0 my (@files_array, @dirs_array, @files_to_be_skipped);
33 11         18 my $size = 0;
34 11         43 my $latest_mtime = 0;
35 11         22 my @base_dirs;
36 11         274 my $finder = File::Find::Object->new({
37             depth => 1,
38             followlink => 0,
39             }, $distdir);
40 11         3647 my %seen; # GH-83
41 11         155 while(defined(my $name = $finder->next)) {
42 54 50       20067 $name =~ s|\\|/|g if $^O eq 'MSWin32';
43 54 50       683 (my $path = $name) =~ s!^\Q$distdir\E(?:/|$)!! or next;
44 54 50       142 next if $path eq '';
45 54 100       276 next if $seen{$path}++;
46              
47 43 100       941 if ($me->d->{is_local_distribution}) {
48 41 50       321 next if $path =~ m!/\.!;
49             }
50              
51 43 50 33     125 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 43 100       575 if (-d $name) {
59 24   50     184 $dirs{$path} ||= {};
60 24 50       228 if (-l $name) {
61 0         0 $dirs{$path}{symlink} = 1;
62             }
63 24         75 push @dirs_array, $path;
64 24         101 next;
65             }
66              
67 19 50       177 if (my $stat = stat($name)) {
68 19   50     3660 $files{$path}{size} = $stat->size || 0;
69 19         235 $size += $files{$path}{size};
70              
71 19         327 my $mtime = $files{$path}{mtime} = $stat->mtime;
72 19 100       185 $latest_mtime = $mtime if $mtime > $latest_mtime;
73             } else {
74 0         0 $files{$path}{stat_error} = $!;
75 0         0 next;
76             }
77              
78 19 100       273 if (-l $name) {
79 2         24 $files{$path}{symlink} = 1;
80             }
81              
82 19 50 33     74 if ($no_index_re && $path =~ qr/$no_index_re/) {
83 0         0 $files{$path}{no_index} = 1;
84 0         0 next;
85             }
86              
87 19 50       237 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 19 50       136 next if $path =~ m!(?:^|/)\.[^/]+/!;
94              
95 19         61 push @files_array, $path;
96              
97             # distribution may have several Makefile.PLs, thus
98             # several 'lib' or 't' directories to care
99 19 50 33     189 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 11         769 $me->d->{size_unpacked} = $size;
106 11         286 $me->d->{latest_mtime} = $latest_mtime;
107              
108 0         0 my @symlinks = sort {$a cmp $b} (
109 19         73 grep({ $files{$_}{symlink} } keys %files),
110 11         95 grep({ $dirs{$_}{symlink} } keys %dirs)
  24         44  
111             );
112              
113 11 100       37 if (@symlinks) {
114 2         67 $me->d->{error}{symlinks} = join ',', @symlinks;
115             }
116              
117 11 50       75 if (@files_to_be_skipped) {
118 0         0 $me->d->{error}{no_files_to_be_skipped} = join ',', @files_to_be_skipped;
119             }
120              
121 11 50       30 $me->d->{base_dirs} = [sort @base_dirs] if @base_dirs;
122 11         44 my $base_dirs_re = join '|', '', map {quotemeta "$_/"} @base_dirs;
  0         0  
123              
124             # find special files/dirs
125 11         140 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 11         39 my @special_dirs = sort (qw(lib t xt));
127              
128 11         435 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 11         61 for my $base_dir ('', @base_dirs) {
134 11 50       51 $base_dir = "$base_dir/" if $base_dir;
135 11         22 for my $name (@special_files) {
136 154         416 my $file = "$base_dir$name";
137 154 100       290 if (exists $files{$file}) {
138 7         41 (my $key = "file_".lc $name) =~ s/\./_/;
139 7 50       155 $me->d->{$key} = $me->d->{$key} ? $me->d->{$key}.",$file" : $file;
140             }
141             }
142 11         24 for my $name (@special_dirs) {
143 33         193 my $dir = "$base_dir$name";
144 33 100       84 if (exists $dirs{$dir}) {
145 6         14 my $key = "dir_$name";
146 6 50       117 $me->d->{$key} = $me->d->{$key} ? $me->d->{$key}.",$dir" : $dir;
147             }
148             }
149             }
150              
151 11         55 for my $file (sort keys %files) {
152 19 100       194 next unless $file =~ m!^(?:$base_dirs_re)[^/]+$!;
153 13         52 while(my ($key, $re) = each %special_files_re) {
154 26 50       158 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 11         209 $me->d->{files} = scalar @files_array;
162 11         273 $me->d->{files_array} = \@files_array;
163 11         241 $me->d->{files_hash} = \%files;
164 11         258 $me->d->{dirs} = scalar @dirs_array;
165 11         258 $me->d->{dirs_array} = \@dirs_array;
166              
167 11         84 my @ignored = grep {$files{$_}{no_index}} sort keys %files;
  19         51  
168 11 50       35 $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 11         28 for my $type (qw/makefile_pl build_pl/) {
173 22   50     517 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 11         360 return;
184             }
185              
186             sub _make_no_index_regex {
187 11     11   36 my ($class, $me) = @_;
188              
189 11         197 my $meta = $me->d->{meta_yml};
190 11 100 66     88 return unless $meta && ref $meta eq ref {};
191              
192 3   33     26 my $no_index = $meta->{no_index} || $meta->{private};
193 3 50 33     14 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 11     11   31 my ($class, $me, $distdir) = @_;
217              
218 11         29 my $maniskip_file = "$distdir/MANIFEST.SKIP";
219 11 50 33     210 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 11 50   11   78 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 11 100   11   84 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 11     11   69 my $d = shift;
275 11 100       50 return 1 if $d->{file_meta_yml};
276 8 50 66     57 return 1 if $d->{is_local_distribution} && $d->{file_mymeta_yml};
277 8         23 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 11     11   58 my $d = shift;
290 11 50       35 return 1 if $d->{file_meta_json};
291 11 50 66     81 return 1 if $d->{is_local_distribution} && $d->{file_mymeta_json};
292 11         34 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 11     11   65 my $d = shift;
306 11 50 33     61 return 1 if $d->{file_makefile_pl} || $d->{file_build_pl};
307 11         29 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 11 50   11   95 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 11 50   11   83 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 11 100   11   106 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 11     11   77 my $d = shift;
350             # TODO: make sure if .t files do exist in t/ directory.
351 11 50 33     213 return 1 if $d->{file_test_pl} || $d->{dir_t};
352 11         38 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 11     11   79 my $d = shift;
366             # TODO: make sure if .t files do exist in t/ directory.
367 11 50 33     97 return 1 if !$d->{file_test_pl} && $d->{dir_t};
368 11         39 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 11     11   71 my $d = shift;
383 11 50 33     96 if ($d->{stdin_in_makefile_pl}||$d->{stdin_in_build_pl}) {
384 0         0 return 0;
385             }
386 11         37 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 11     11   67 my $d = shift;
401 11 50       33 if ($d->{error}{no_maniskip_error}) {
402 0         0 return 0;
403             }
404 11         25 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 554 ];
412             }
413              
414              
415             q{Favourite record of the moment:
416             Fat Freddys Drop: Based on a true story};
417              
418              
419             __END__