File Coverage

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


line stmt bran cond sub pod time code
1             package Module::CPANTS::Kwalitee::Files;
2 7     7   3848 use warnings;
  7         18  
  7         230  
3 7     7   48 use strict;
  7         26  
  7         146  
4 7     7   3500 use File::Find::Object;
  7         79441  
  7         218  
5 7     7   58 use File::Spec::Functions qw(catfile);
  7         17  
  7         368  
6 7     7   47 use File::stat;
  7         19  
  7         62  
7 7     7   4168 use ExtUtils::Manifest qw(maniskip);
  7         44539  
  7         18921  
8             $ExtUtils::Manifest::Quiet = 1;
9              
10             our $VERSION = '1.00';
11             $VERSION =~ s/_//; ## no critic
12              
13             our $RespectManiskip = 1; # for Test::Kwalitee and its friends
14              
15 35     35 1 88 sub order { 15 }
16              
17             ##################################################################
18             # Analyse
19             ##################################################################
20              
21             sub analyse {
22 11     11 1 52 my $class = shift;
23 11         29 my $me = shift;
24 11         232 my $distdir = $me->distdir;
25 11 50       94 $distdir =~ s|\\|/|g if $^O eq 'MSWin32';
26              
27             # Respect no_index if possible
28 11         56 my $no_index_re = $class->_make_no_index_regex($me);
29 11         93 my $maniskip = $class->_make_maniskip($me, $distdir);
30              
31 11         102 my (%files, %dirs);
32 11         0 my (@files_array, @dirs_array, @files_to_be_skipped);
33 11         37 my $size = 0;
34 11         53 my $latest_mtime = 0;
35 11         26 my @base_dirs;
36 11         307 my $finder = File::Find::Object->new({
37             depth => 1,
38             followlink => 0,
39             }, $distdir);
40 11         4191 my %seen; # GH-83
41 11         212 while(defined(my $name = $finder->next)) {
42 54 50       22820 $name =~ s|\\|/|g if $^O eq 'MSWin32';
43 54 50       831 (my $path = $name) =~ s!^\Q$distdir\E(?:/|$)!! or next;
44 54 50       169 next if $path eq '';
45 54 100       350 next if $seen{$path}++;
46              
47 43 100       1092 if ($me->d->{is_local_distribution}) {
48 41 50       352 next if $path =~ m!/\.!;
49             }
50              
51 43 50 33     137 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       663 if (-d $name) {
59 24   50     206 $dirs{$path} ||= {};
60 24 50       253 if (-l $name) {
61 0         0 $dirs{$path}{symlink} = 1;
62             }
63 24         82 push @dirs_array, $path;
64 24         122 next;
65             }
66              
67 19 50       202 if (my $stat = stat($name)) {
68 19   50     4123 $files{$path}{size} = $stat->size || 0;
69 19         250 $size += $files{$path}{size};
70              
71 19         339 my $mtime = $files{$path}{mtime} = $stat->mtime;
72 19 100       201 $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       306 if (-l $name) {
79 2         28 $files{$path}{symlink} = 1;
80             }
81              
82 19 50 33     88 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       245 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       146 next if $path =~ m!(?:^|/)\.[^/]+/!;
94              
95 19         69 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     259 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         843 $me->d->{size_unpacked} = $size;
106 11         274 $me->d->{latest_mtime} = $latest_mtime;
107              
108 0         0 my @symlinks = sort {$a cmp $b} (
109 19         79 grep({ $files{$_}{symlink} } keys %files),
110 11         134 grep({ $dirs{$_}{symlink} } keys %dirs)
  24         55  
111             );
112              
113 11 100       44 if (@symlinks) {
114 2         55 $me->d->{error}{symlinks} = join ',', @symlinks;
115             }
116              
117 11 50       65 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       35 $me->d->{base_dirs} = [sort @base_dirs] if @base_dirs;
122 11         172 my $base_dirs_re = join '|', '', map {quotemeta "$_/"} @base_dirs;
  0         0  
123              
124             # find special files/dirs
125 11         279 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         61 my @special_dirs = sort (qw(lib t xt));
127              
128 11         485 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         70 for my $base_dir ('', @base_dirs) {
134 11 50       54 $base_dir = "$base_dir/" if $base_dir;
135 11         33 for my $name (@special_files) {
136 154         533 my $file = "$base_dir$name";
137 154 100       347 if (exists $files{$file}) {
138 7         42 (my $key = "file_".lc $name) =~ s/\./_/;
139 7 50       190 $me->d->{$key} = $me->d->{$key} ? $me->d->{$key}.",$file" : $file;
140             }
141             }
142 11         34 for my $name (@special_dirs) {
143 33         242 my $dir = "$base_dir$name";
144 33 100       96 if (exists $dirs{$dir}) {
145 6         16 my $key = "dir_$name";
146 6 50       190 $me->d->{$key} = $me->d->{$key} ? $me->d->{$key}.",$dir" : $dir;
147             }
148             }
149             }
150              
151 11         53 for my $file (sort keys %files) {
152 19 100       226 next unless $file =~ m!^(?:$base_dirs_re)[^/]+$!;
153 13         62 while(my ($key, $re) = each %special_files_re) {
154 26 50       186 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         239 $me->d->{files} = scalar @files_array;
162 11         328 $me->d->{files_array} = \@files_array;
163 11         294 $me->d->{files_hash} = \%files;
164 11         272 $me->d->{dirs} = scalar @dirs_array;
165 11         292 $me->d->{dirs_array} = \@dirs_array;
166              
167 11         99 my @ignored = grep {$files{$_}{no_index}} sort keys %files;
  19         74  
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         32 for my $type (qw/makefile_pl build_pl/) {
173 22   50     655 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         384 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     120 return unless $meta && ref $meta eq ref {};
191              
192 3   33     39 my $no_index = $meta->{no_index} || $meta->{private};
193 3 50 33     31 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   54 my ($class, $me, $distdir) = @_;
217              
218 11         52 my $maniskip_file = "$distdir/MANIFEST.SKIP";
219 11 50 33     217 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 unlink $maniskip_bak_file; # probably generated by #include_default
236             }
237 0         0 $maniskip;
238             }
239              
240             ##################################################################
241             # Kwalitee Indicators
242             ##################################################################
243              
244             sub kwalitee_indicators {
245             return [
246             {
247             name => 'has_readme',
248             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.},
249             remedy => q{Add a README to the distribution. It should contain a quick description of your module and how to install it.},
250 11 50   11   99 code => sub { shift->{file_readme} ? 1 : 0 },
251             details => sub {
252 0     0   0 my $d = shift;
253 0         0 return "README was not found.";
254             },
255             },
256             {
257             name => 'has_manifest',
258             error => q{The file "MANIFEST" is missing from this distribution. The MANIFEST lists all files included in the distribution.},
259             remedy => q{Add a MANIFEST to the distribution. Your buildtool should be able to autogenerate it (eg "make manifest" or "./Build manifest")},
260 11 100   11   140 code => sub { shift->{file_manifest} ? 1 : 0 },
261             details => sub {
262 0     0   0 my $d = shift;
263 0         0 return "MANIFEST was not found.";
264             },
265             },
266             {
267             name => 'has_meta_yml',
268             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.},
269             remedy => q{Add a META.yml to the distribution. Your buildtool should be able to autogenerate it.},
270             code => sub {
271 11     11   66 my $d = shift;
272 11 100       50 return 1 if $d->{file_meta_yml};
273 8 50 66     69 return 1 if $d->{is_local_distribution} && $d->{file_mymeta_yml};
274 8         26 return 0;
275             },
276             details => sub {
277 0     0   0 my $d = shift;
278 0         0 return "META.yml was not found.";
279             },
280             },
281             {
282             name => 'has_meta_json',
283             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.},
284             remedy => q{Add a META.json to the distribution. Your buildtool should be able to autogenerate it.},
285             code => sub {
286 11     11   64 my $d = shift;
287 11 50       74 return 1 if $d->{file_meta_json};
288 11 50 66     74 return 1 if $d->{is_local_distribution} && $d->{file_mymeta_json};
289 11         35 return 0;
290             },
291             details => sub {
292 0     0   0 my $d = shift;
293 0         0 return "META.json was not found.";
294             },
295             is_extra => 1,
296             },
297             {
298             name => 'has_buildtool',
299             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.},
300             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.},
301             code => sub {
302 11     11   68 my $d = shift;
303 11 50 33     123 return 1 if $d->{file_makefile_pl} || $d->{file_build_pl};
304 11         41 return 0;
305             },
306             details => sub {
307 0     0   0 my $d = shift;
308 0         0 return "Neither Makefile.PL nor Build.PL was found.";
309             },
310             },
311             {
312             name => 'has_changelog',
313             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.},
314             remedy => q{Add a Changelog (best named 'Changes') to the distribution. It should list at least major changes implemented in newer versions.},
315 11 50   11   85 code => sub { shift->{file_changelog} ? 1 : 0 },
316             details => sub {
317 0     0   0 my $d = shift;
318 0         0 return "Any Changelog file was not found.";
319             },
320             },
321             {
322             name => 'no_files_to_be_skipped',
323             error => q{This distribution contains files that should be skipped by MANIFEST.SKIP.},
324             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.},
325 11 50   11   89 code => sub {shift->{error}{no_files_to_be_skipped} ? 0 : 1},
326             details => sub {
327 0     0   0 my $d = shift;
328 0         0 return "The following files were found: ".$d->{error}{no_files_to_be_skipped};
329             },
330             },
331             {
332             name => 'no_symlinks',
333             error => q{This distribution includes symbolic links (symlinks). This is bad, because there are operating systems that do not handle symlinks.},
334             remedy => q{Remove the symlinks from the distribution.},
335 11 100   11   99 code => sub {shift->{error}{symlinks} ? 0 : 1},
336             details => sub {
337 0     0   0 my $d = shift;
338 0         0 return "The following symlinks were found: ".$d->{error}{symlinks};
339             },
340             },
341             {
342             name => 'has_tests',
343             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!},
344             remedy => q{Add tests!},
345             code => sub {
346 11     11   73 my $d = shift;
347             # TODO: make sure if .t files do exist in t/ directory.
348 11 50 33     124 return 1 if $d->{file_test_pl} || $d->{dir_t};
349 11         42 return 0;
350             },
351             details => sub {
352 0     0   0 my $d = shift;
353 0         0 return q{Neither "test.pl" nor "t/" directory was not found.};
354             },
355             },
356             {
357             name => 'has_tests_in_t_dir',
358             is_extra => 1,
359             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.},
360             remedy => q{Add tests or move tests.pl to the t/ directory!},
361             code => sub {
362 11     11   96 my $d = shift;
363             # TODO: make sure if .t files do exist in t/ directory.
364 11 50 33     102 return 1 if !$d->{file_test_pl} && $d->{dir_t};
365 11         39 return 0;
366             },
367             details => sub {
368 0     0   0 my $d = shift;
369 0 0       0 return q{"test.pl" was found.} if $d->{file_test_pl};
370 0         0 return q{"t/" directory was not found.};
371             },
372             },
373             {
374             name => 'no_stdin_for_prompting',
375             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.},
376             is_extra => 1,
377             remedy => q{Use the prompt() method from ExtUtils::MakeMaker/Module::Build.},
378             code => sub {
379 11     11   101 my $d = shift;
380 11 50 33     102 if ($d->{stdin_in_makefile_pl}||$d->{stdin_in_build_pl}) {
381 0         0 return 0;
382             }
383 11         38 return 1;
384             },
385             details => sub {
386 0     0   0 my $d = shift;
387 0 0       0 return " was found in Makefile.PL" if $d->{stdin_in_makefile_pl};
388 0 0       0 return " was found in Build.PL" if $d->{stdin_in_build_pl};
389             },
390             },
391             {
392             name => 'no_maniskip_error',
393             error => q{This distribution's MANIFEST.SKIP has a problematic entry.},
394             is_extra => 1,
395             remedy => q{Fix the problematic entry.},
396             code => sub {
397 11     11   72 my $d = shift;
398 11 50       40 if ($d->{error}{no_maniskip_error}) {
399 0         0 return 0;
400             }
401 11         25 return 1;
402             },
403             details => sub {
404 0     0   0 my $d = shift;
405 0         0 return $d->{error}{no_maniskip_error};
406             },
407             },
408 8     8 1 653 ];
409             }
410              
411              
412             q{Favourite record of the moment:
413             Fat Freddys Drop: Based on a true story};
414              
415              
416             __END__