| File: | blib/lib/App/Project/Doctor/Check/CpanReadiness.pm |
| Coverage: | 96.8% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package App::Project::Doctor::Check::CpanReadiness; | |||||
| 2 | ||||||
| 3 | 3 3 3 | 6 3 36 | use strict; | |||
| 4 | 3 3 3 | 7 1 83 | use warnings; | |||
| 5 | 3 3 3 | 7 2 12 | use autodie qw(:all); | |||
| 6 | ||||||
| 7 | 3 3 3 | 6312 3 15 | use parent -norequire, 'App::Project::Doctor::Check::Base'; | |||
| 8 | ||||||
| 9 | 3 3 3 | 98 3 69 | use Carp qw(croak carp); | |||
| 10 | 3 3 3 | 28 2 1306 | use Readonly; | |||
| 11 | ||||||
| 12 | our $VERSION = '0.02'; | |||||
| 13 | ||||||
| 14 | Readonly::Scalar my $VERSION_RE => qr/^\d+\.\d+(?:\.\d+)?(?:_\d+)?$/; | |||||
| 15 | # Changes and MANIFEST must use these exact names; README accepts variants below. | |||||
| 16 | Readonly::Array my @REQUIRED_FILES => qw(Changes MANIFEST); | |||||
| 17 | # CPAN and GitHub both accept any of these forms as the distribution README. | |||||
| 18 | Readonly::Array my @README_VARIANTS => qw(README README.md README.pod README.rst README.txt); | |||||
| 19 | ||||||
| 20 | 4 | 180 | sub name { 'CPAN Readiness' } | |||
| 21 | 1 | 3 | sub description { 'Version format, Changes, MANIFEST, and a README variant are present.' } | |||
| 22 | 2 | 4 | sub can_fix { 0 } | |||
| 23 | 2 | 4 | sub order { 90 } | |||
| 24 | ||||||
| 25 | sub check { | |||||
| 26 | 32 | 38 | my ($self, $ctx) = @_; | |||
| 27 | 32 | 43 | croak 'check requires an App::Project::Doctor::Context' unless ref $ctx; | |||
| 28 | ||||||
| 29 | 32 | 25 | my @findings; | |||
| 30 | ||||||
| 31 | # Version format check. | |||||
| 32 | 32 | 36 | my $version = _read_version($ctx); | |||
| 33 | 32 | 50 | if (defined $version) { | |||
| 34 | 25 | 104 | if ($version !~ $VERSION_RE) { | |||
| 35 | 5 | 10 | push @findings, _f( | |||
| 36 | severity => 'error', | |||||
| 37 | message => "Version '$version' does not match CPAN format (X.YY or X.YY.ZZ).", | |||||
| 38 | ); | |||||
| 39 | } | |||||
| 40 | } else { | |||||
| 41 | 7 | 10 | push @findings, _f( | |||
| 42 | severity => 'warning', | |||||
| 43 | message => 'Could not determine distribution version from any module.', | |||||
| 44 | ); | |||||
| 45 | } | |||||
| 46 | ||||||
| 47 | # Required release files (exact names required by CPAN toolchain). | |||||
| 48 | 32 | 60 | for my $file (@REQUIRED_FILES) { | |||
| 49 | 64 | 174 | unless ($ctx->has_file($file)) { | |||
| 50 | 22 | 31 | push @findings, _f( | |||
| 51 | severity => 'error', | |||||
| 52 | message => "'$file' is missing from the distribution root.", | |||||
| 53 | ); | |||||
| 54 | } | |||||
| 55 | } | |||||
| 56 | ||||||
| 57 | # README is required but any common variant is acceptable. README.md is the | |||||
| 58 | # norm on GitHub; CPAN itself accepts all of these without complaint. | |||||
| 59 | 32 160 | 82 190 | unless (grep { $ctx->has_file($_) } @README_VARIANTS) { | |||
| 60 | 12 | 17 | push @findings, _f( | |||
| 61 | severity => 'error', | |||||
| 62 | message => 'README is missing -- none of ' . join(', ', @README_VARIANTS) . ' found.', | |||||
| 63 | ); | |||||
| 64 | } | |||||
| 65 | ||||||
| 66 | # Changes file must have at least one version entry. | |||||
| 67 | 32 | 53 | if ($ctx->has_file('Changes')) { | |||
| 68 | 21 | 25 | my $content = $ctx->slurp('Changes'); | |||
| 69 | 21 | 73 | unless ($content =~ /^\d+\.\d+/m || $content =~ /^v\d+/m) { | |||
| 70 | 1 | 3 | push @findings, _f( | |||
| 71 | severity => 'warning', | |||||
| 72 | message => 'Changes file has no version entries.', | |||||
| 73 | file => 'Changes', | |||||
| 74 | ); | |||||
| 75 | } | |||||
| 76 | } | |||||
| 77 | ||||||
| 78 | # MANIFEST stale-check requires 'make manifest' -- too invasive; just advise. | |||||
| 79 | 32 | 37 | if ($ctx->has_file('MANIFEST')) { | |||
| 80 | 21 | 24 | push @findings, _f( | |||
| 81 | severity => 'info', | |||||
| 82 | message => "MANIFEST present -- run 'make manifest' to verify it is not stale.", | |||||
| 83 | ); | |||||
| 84 | } | |||||
| 85 | ||||||
| 86 | # Emit a pass only when there are no errors or warnings. | |||||
| 87 | 32 68 | 32 61 | my $has_problem = grep { $_->severity =~ /^(?:error|warning)$/ } @findings; | |||
| 88 | 32 | 31 | unless ($has_problem) { | |||
| 89 | 15 | 19 | push @findings, _f( | |||
| 90 | severity => 'pass', | |||||
| 91 | message => 'Distribution meets basic CPAN readiness requirements.', | |||||
| 92 | ); | |||||
| 93 | } | |||||
| 94 | ||||||
| 95 | 32 | 58 | return @findings; | |||
| 96 | } | |||||
| 97 | ||||||
| 98 | # --------------------------------------------------------------------------- | |||||
| 99 | # Private helpers | |||||
| 100 | # --------------------------------------------------------------------------- | |||||
| 101 | ||||||
| 102 | sub _f { | |||||
| 103 | 83 | 242 | require App::Project::Doctor::Finding; | |||
| 104 | 83 | 154 | return App::Project::Doctor::Finding->new(check_name => 'CPAN Readiness', @_); | |||
| 105 | } | |||||
| 106 | ||||||
| 107 | sub _read_version { | |||||
| 108 | 33 | 24 | my $ctx = shift; | |||
| 109 | 33 33 | 29 57 | for my $mod (@{ $ctx->lib_modules }) { | |||
| 110 | 27 27 | 29 36 | my $content = eval { $ctx->slurp($mod) } // next; | |||
| 111 | 27 | 105 | if (my ($v) = $content =~ /^\s*our\s+\$VERSION\s*=\s*['"]?([^'";\s]+)['"]?/m) { | |||
| 112 | 26 | 40 | return $v; | |||
| 113 | } | |||||
| 114 | } | |||||
| 115 | 7 | 11 | return undef; | |||
| 116 | } | |||||
| 117 | ||||||
| 118 | 1; | |||||
| 119 | ||||||