lib/App/Project/Doctor/Check/Meta.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 91.67%
TER3 (LCSAJ): 100.0% (4/4)
Approximate LCSAJ segments: 13

LCSAJ Legend

Covered — this LCSAJ path was executed during testing.

Not covered — this LCSAJ path was never executed. These are the paths to focus on.

Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:

        start → end → jump
        

Uncovered paths show [NOT COVERED] in the tooltip.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    1: package App::Project::Doctor::Check::Meta;
    2: 
    3: use strict;
    4: use warnings;
    5: use autodie qw(:all);
    6: 
    7: use parent -norequire, 'App::Project::Doctor::Check::Base';
    8: 
    9: use Carp qw(croak carp);
   10: use Readonly;
   11: 
   12: our $VERSION = '0.02';
   13: 
   14: Readonly::Array my @REQUIRED_FIELDS => qw(name version author abstract license);
   15: Readonly::Array my @META_FILES      => qw(META.json META.yml MYMETA.json MYMETA.yml);
   16: 
   17: sub name        { 'META' }
   18: sub description { 'META.yml or META.json is present, parseable, and complete.' }
   19: sub can_fix     { 0 }
   20: sub order       { 30 }
   21: 
   22: sub check {
23 → 30 → 46   23: 	my ($self, $ctx) = @_;
   24: 	croak 'check requires an App::Project::Doctor::Context' unless ref $ctx;
   25: 
   26: 	my @findings;
   27: 
   28: 	my ($meta_file) = grep { $ctx->has_file($_) } @META_FILES;
   29: 
   30: 	unless ($meta_file) {

Mutants (Total: 1, Killed: 1, Survived: 0)

31: push @findings, _f( 32: severity => 'warning', 33: message => 'No META.{yml,json} -- run your builder to generate one.', 34: detail => 'CPAN indexers require META to discover name and version.', 35: ); 36: # Fall back: at least confirm a builder file exists. 37: unless ($ctx->builder_file) {

Mutants (Total: 1, Killed: 1, Survived: 0)

38: push @findings, _f( 39: severity => 'error', 40: message => 'No Makefile.PL, Build.PL, or dist.ini found.', 41: ); 42: } 43: return @findings;

Mutants (Total: 2, Killed: 2, Survived: 0)

44: } 45: 46 → 48 → 56 46: require CPAN::Meta; 47: my $meta = eval { CPAN::Meta->load_file($ctx->abs_path($meta_file)) }; 48: if ($@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

49: return _f(

Mutants (Total: 2, Killed: 2, Survived: 0)

50: severity => 'error', 51: message => "Failed to parse $meta_file -- file may be malformed.", 52: file => $meta_file, 53: ); 54: } 55: 56 → 57 → 66 56: my %data = %{ $meta->as_struct }; 57: for my $field (@REQUIRED_FIELDS) { 58: next if defined $data{$field} && length $data{$field}; 59: push @findings, _f( 60: severity => 'error', 61: message => "META field '$field' is missing or empty in $meta_file.", 62: file => $meta_file, 63: ); 64: } 65: 66 → 66 → 73 66: unless (@findings) {

Mutants (Total: 1, Killed: 1, Survived: 0)

67: push @findings, _f( 68: severity => 'pass', 69: message => "$meta_file is valid and all required fields are present.", 70: ); 71: } 72: 73: return @findings;

Mutants (Total: 2, Killed: 2, Survived: 0)

74: } 75: 76: sub _f { 77: require App::Project::Doctor::Finding; 78: return App::Project::Doctor::Finding->new(check_name => 'META', @_);

Mutants (Total: 2, Killed: 2, Survived: 0)

79: } 80: 81: 1; 82: 83: __END__ 84: 85: =head1 NAME 86: 87: App::Project::Doctor::Check::Meta - Check META file presence and validity 88: 89: =head1 DESCRIPTION 90: 91: Uses L<CPAN::Meta> to parse the distribution META file and verifies that all 92: required fields (name, version, author, abstract, license) are present. 93: 94: =head1 METHODS 95: 96: =head2 check( $context ) 97: 98: Locates the first available META file, parses it, and validates required fields. 99: 100: =head3 API SPECIFICATION 101: 102: =head4 Input 103: 104: $context : App::Project::Doctor::Context 105: 106: =head4 Output 107: 108: List of App::Project::Doctor::Finding -- 109: warning when no META.* file is found (builder file present), 110: warning + error when no META.* and no builder file found, 111: error when the META file cannot be parsed, 112: one error per missing required field (name/version/author/abstract/license), 113: pass when all required fields are present. 114: 115: =head3 MESSAGES 116: 117: Code | Trigger | Resolution 118: -----|-----------------------------|----------------------------------------- 119: M001 | No META.* file found | Run builder to generate (make or dzil) 120: M002 | META parse failure | Correct malformed YAML/JSON by hand 121: M003 | Required META field missing | Add field to Makefile.PL / dist.ini 122: 123: =head3 FORMAL SPECIFICATION 124: 125: check : Context -> [Finding] 126: check ctx == 127: let f = first meta_file existing in ctx 128: in if f = undef then [warning] ++ (if no builder then [error] else []) 129: else let m = parse f 130: in if parse_fails then [error] 131: else [error per missing field] ++ (if all ok then [pass] else []) 132: 133: =head1 AUTHOR 134: 135: Nigel Horne C<< <njh@nigelhorne.com> >> 136: 137: =head1 LICENSE 138: 139: Copyright (C) 2026 Nigel Horne. 140: This library is free software; you can redistribute it and/or modify 141: it under the same terms as Perl itself. 142: 143: =cut