File Coverage

blib/lib/Pod/Coverage/TrustMe.pm
Criterion Covered Total %
statement 195 198 98.4
branch 52 74 70.2
condition 33 43 76.7
subroutine 38 38 100.0
pod 10 11 90.9
total 328 364 90.1


line stmt bran cond sub pod time code
1             package Pod::Coverage::TrustMe;
2 10     10   659700 use strict;
  10         103  
  10         307  
3 10     10   55 use warnings;
  10         18  
  10         541  
4              
5             our $VERSION = '0.001002';
6             $VERSION =~ tr/_//d;
7              
8 10     10   4745 use Pod::Coverage::TrustMe::Parser;
  10         28  
  10         352  
9 10     10   72 use B ();
  10         20  
  10         203  
10 10     10   56 use Carp qw(croak);
  10         19  
  10         720  
11 10 50   10   75 use constant _GVf_IMPORTED_CV => defined &B::GVf_IMPORTED_CV ? B::GVf_IMPORTED_CV() : 0x80;
  10         22  
  10         2544  
12              
13 10         21 use constant DEFAULT_PRIVATE => do {
14 10         19 my %s;
15             [
16             qr/\A_/,
17             # anything with non-word characters is not standard syntax, so exclude
18             # them. this includes overloads, which are internally stored as methods
19             # starting with '('.
20             qr/\W/,
21 10         16632 (map qr{\A\Q$_\E\z}, grep !$s{$_}++, qw(
22             import
23             unimport
24              
25             can
26             isa
27             does
28             DOES
29              
30             AUTOLOAD
31              
32             DESTROY
33             CLONE
34             CLONE_SKIP
35              
36             BUILD
37             BUILDALL
38             DEMOLISH
39             DEMOLISHALL
40              
41             bootstrap
42              
43             TIESCALAR
44             FETCH STORE
45              
46             TIEARRAY
47             FETCH STORE FETCHSIZE STORESIZE EXTEND EXISTS
48             DELETE CLEAR PUSH POP SHIFT UNSHIFT SPLICE
49              
50             TIEHASH
51             FETCH STORE DELETE CLEAR EXISTS FIRSTKEY NEXTKEY SCALAR
52              
53             TIEHANDLE
54             OPEN BINMODE FILENO SEEK TELL WRITE PRINT PRINTF
55             READ READLINE GETC EOF CLOSE
56              
57             UNTIE
58             )),
59             qr/\A
60             (?: MODIFY | FETCH )
61             _
62             (?: REF | SCALAR | ARRAY | HASH | CODE | GLOB | FORMAT | IO )
63             _
64             ATTRIBUTES
65             \z/x,
66             ];
67 10     10   77 };
  10         19  
68             &Internals::SvREADONLY(+DEFAULT_PRIVATE, 1);
69              
70             our $PACKAGE_RE = qr{
71             (?=[^0-9'])
72             (?:
73             ::
74             |
75             \w*
76             (?:'[^\W0-9]\w*)*
77             )*
78             }x;
79             &Internals::SvREADONLY(\$PACKAGE_RE, 1);
80              
81             my %DEFAULTS = (
82             trust_roles => 1,
83             trust_parents => 1,
84             trust_pod => 1,
85             require_link => 0,
86             export_only => 0,
87             ignore_imported => 1,
88             nonwhitespace => 0,
89             trustme => [],
90             private => DEFAULT_PRIVATE,
91             pod_from => undef,
92             package => undef,
93             );
94              
95             sub new {
96 39     39 0 7717 my ($class, %args) = @_;
97 39 50       143 $class = ref $class
98             if ref $class;
99              
100             my $new = {
101 39 100       781 map +($_ => exists $args{$_} ? $args{$_} : $DEFAULTS{$_}), keys %DEFAULTS,
102             };
103              
104 39 100 100     307 if (exists $args{private} || exists $args{also_private}) {
105             $new->{private} = [
106             map +(ref $_ ? $_ : qr/\A\Q$_\E\z/), (
107 4         43 @{ $new->{private} },
108 4 100       8 exists $args{also_private} ? @{ $args{also_private} } : (),
  1 100       40  
109             )
110             ];
111             }
112              
113             my $package = $new->{package}
114 39 50       139 or die "package is a required parameter";
115              
116 39 100       75 eval { require(__pack_to_pm($package)); 1 } or do {
  39         122  
  38         4491  
117 1         353 $new->{why_unrated} = "requiring '$package' failed: $@";
118 1         4 $new->{broken} = 1;
119             };
120              
121 39         590 bless $new, $class;
122             }
123              
124             sub package {
125 296     296 1 542 my $self = shift;
126 296         1139 $self->{package};
127             }
128              
129             sub symbols {
130 51     51 1 141 my $self = shift;
131             return undef
132 51 100       228 if $self->{broken};
133 50   66     204 $self->{symbols} ||= do {
134 38         106 my $package = $self->package;
135              
136 38         76 my %pods = map +( $_ => 1 ), @{ $self->_get_pods($package) };
  38         120  
137             my %symbols = map +(
138 38   100     190 $_ => ($pods{$_} || $self->_trustme_check($_) || 0),
139             ), $self->_get_syms($package);
140              
141 38 100       210 if (!grep $_, values %symbols) {
142 3   50     29 $self->{why_unrated} ||= "no public symbols defined";
143             }
144              
145 38         256 \%symbols;
146             };
147             }
148              
149             sub coverage {
150 42     42 1 2167 my $self = shift;
151 42 100       123 my $symbols = $self->symbols
152             or return undef;
153              
154 41 100       152 my $total = scalar keys %$symbols
155             or return undef;
156 40         125 my $documented = scalar grep $_, values %$symbols;
157              
158 40         306 return $documented / $total;
159             }
160              
161             sub why_unrated {
162 2     2 1 6 my $self = shift;
163 2         17 return $self->{why_unrated};
164             }
165              
166             sub uncovered {
167 7     7 1 12 my $self = shift;
168 7 50       20 my $symbols = $self->symbols
169             or return ();
170 7         44 my @uncovered = sort grep !$symbols->{$_}, keys %$symbols;
171 7         34 return @uncovered;
172             }
173             sub naked {
174 4     4 1 522 my $self = shift;
175 4         13 return $self->uncovered(@_);
176             }
177              
178             sub covered {
179 2     2 1 6 my $self = shift;
180 2 50       8 my $symbols = $self->symbols
181             or return ();
182 2         16 my @covered = sort grep $symbols->{$_}, keys %$symbols;
183 2         13 return @covered;
184             }
185              
186             sub report {
187 2     2 1 4 my $self = shift;
188 2         8 my $rating = $self->coverage;
189              
190 2 50       5 $rating = 'unrated (' . $self->why_unrated . ')'
191             unless defined $rating;
192              
193 2         8 my $message = sprintf "%s has a Pod coverage rating of %s\n", $self->package, $rating;
194              
195 2         11 my @uncovered = $self->uncovered;
196 2 50       9 if (@uncovered) {
197 2         6 $message .= "The following are uncovered:\n";
198             $message .= " $_\n"
199 2         11 for @uncovered;
200             }
201 2         220 return $message;
202             }
203              
204             sub print_report {
205 2     2 1 5 my $self = shift;
206 2         9 print $self->report;
207             }
208              
209             sub import {
210 8     8   58 my $class = shift;
211             return
212 8 50       12549 if !@_;
213              
214 0 0       0 $class->new(@_ == 1 ? (package => $_[0]) : @_)->print_report;
215 0         0 return;
216             }
217              
218              
219             sub _search_packages {
220 76     76   156 my $self = shift;
221 76         178 my @search = @_;
222 76 50       278 @search = ('main')
223             if !@search;
224              
225             s/\A(?:::)?(?:(?:main)?::)+//, s/(?:::)?\z/::/
226 76         743 for @search;
227              
228 76         150 my @packages;
229              
230 76         187 while (@search) {
231 18390         28490 my $search = shift @search;
232 18390         30376 push @packages, $search;
233 18390 100       31779 my $base = $search eq 'main::' ? '' : $search;
234              
235 10     10   98 no strict 'refs';
  10         28  
  10         10321  
236 18390   100     309977 my @add =
237             map $base.$_,
238             sort
239             grep /::$/ && $_ ne 'main::',
240             keys %$search;
241              
242 18390         58512 unshift @search, @add;
243             }
244              
245             s/::\z//
246 76         17862 for @packages;
247              
248 76   100     90645 return grep +(
249             $_ ne 'main'
250             && $_ ne ''
251             && $_ ne 'UNIVERSAL'
252             ), @packages;
253             }
254              
255             sub _get_roles {
256 38     38   68 my $self = shift;
257 38         80 my $package = $self->package;
258 38 50       565 my $does
    50          
259             = $package->can('does') ? 'does'
260             : $package->can('DOES') ? 'DOES'
261             : return;
262 38   100     125 return grep $_ ne $package && $package->$does($_), $self->_search_packages;
263             }
264              
265             sub _get_parents {
266 38     38   153 my $self = shift;
267 38         141 my $package = $self->package;
268 38   100     118 return grep $_ ne $package && $package->isa($_), $self->_search_packages;
269             }
270              
271             sub __pack_to_pm {
272 89     89   197 my ($package) = @_;
273 89 50       2487 croak "Invalid package '$package'"
274             unless $package =~ /\A$PACKAGE_RE\z/;
275 89         569 (my $mod = "$package.pm") =~ s{'|::}{/}g;
276 89         12238 return $mod;
277             }
278              
279             sub _pod_for {
280 50     50   93 my $self = shift;
281 50         116 my ($package) = @_;
282 50 50 66     159 if ($self->package eq $package && defined $self->{pod_from}) {
283 0         0 return $self->{pod_from};
284             }
285              
286 50         158 my $mod = __pack_to_pm($package);
287 50 50       234 my $full = $INC{$mod} or return;
288 50         242 (my $maybe_pod = $full) =~ s{\.pm\z}{.pod};
289 50 50       2892 my $pod
    100          
290             = -e $maybe_pod ? $maybe_pod
291             : -e $full ? $full
292             : undef
293             ;
294 50 100       223 if ($self->package eq $package) {
295 38         114 $self->{pod_from} = $pod;
296             }
297 50         543 return $pod;
298             }
299              
300             sub trusted_packages {
301 38     38 1 68 my $self = shift;
302              
303 38         95 my %to_parse = (
304             $self->package => 1,
305             );
306             @to_parse{$self->_get_roles} = ()
307 38 50       184 if $self->{trust_roles};
308             @to_parse{$self->_get_parents} = ()
309 38 50       2113 if $self->{trust_parents};
310              
311 38         391 my @trusted = sort keys %to_parse;
312 38         193 return @trusted;
313             }
314              
315 50     50   345 sub _pod_parser_class { 'Pod::Coverage::TrustMe::Parser' }
316             sub _new_pod_parser {
317 50     50   107 my $self = shift;
318              
319 50         146 my $parser = $self->_pod_parser_class->new(@_);
320 50 100       245 if ($self->{nonwhitespace}) {
321 1         15 $parser->ignore_empty(1);
322             }
323 50         119 return $parser;
324             }
325             sub _pod_parser_for {
326 50     50   108 my $self = shift;
327 50         125 my ($pack) = @_;
328 50 50       196 my $pod = $self->_pod_for($pack)
329             or return undef;
330 50         189 my $parser = $self->_new_pod_parser;
331 50         215 $parser->parse_file($pod);
332 50         1352 return $parser;
333             }
334              
335             sub _parsed {
336 57     57   119 my $self = shift;
337             return $self->{_parsed}
338 57 100       185 if $self->{_parsed};
339              
340             my %parsed = map {
341 38         122 my $pack = $_;
  50         117  
342 50         177 my $parser = $self->_pod_parser_for($pack);
343 50 50       376 $parser ? ($pack => $parser) : ();
344             } $self->trusted_packages;
345              
346 38 100       173 if ($self->{require_link}) {
347 4         12 my $package = $self->package;
348 4         8 my %allowed;
349             my %find_links = (
350 4   33     14 $package => delete $parsed{$package} || $self->_pod_parser_for($package),
351             );
352              
353 4         11 while (%find_links) {
354 6         19 @allowed{keys %find_links} = values %find_links;
355             %find_links =
356             map +(exists $parsed{$_} ? ($_ => delete $parsed{$_}) : ()),
357 6 50       12 map @{ $_->links },
  6         16  
358             values %find_links;
359             }
360              
361 4         13 %parsed = %allowed;
362             }
363              
364 38         163 $self->{_parsed} = \%parsed;
365             }
366              
367             sub _symbols_for {
368 38     38   75 my $self = shift;
369 38         86 my ($package) = @_;
370              
371 38         63 my @symbols;
372 10     10   90 no strict 'refs';
  10         22  
  10         5598  
373              
374 38 100       118 if ($self->{export_only}) {
375             @symbols = (
376 1         7 @{"${package}::EXPORT"},
377 1         2 @{"${package}::EXPORT_OK"},
  1         6  
378             );
379             }
380             else {
381             @symbols =
382             grep !(
383             $self->{ignore_imported} && $self->_imported_check($_)
384             or $self->_private_check($_)
385             ),
386             grep !/::\z/ && defined &{$package.'::'.$_},
387 37   100     80 keys %{$package.'::'};
  37   100     330  
388             }
389              
390 38         209 return @symbols;
391             }
392              
393             sub _get_syms {
394 38     38   86 my $self = shift;
395 38   33     159 my $syms = $self->{_syms} ||= do {
396             # recurse option?
397 38         129 [ $self->_symbols_for($self->package) ];
398             };
399 38         382 return @$syms;
400             }
401              
402             sub _get_pods {
403 38     38   73 my $self = shift;
404              
405 38   33     121 $self->{_pods} ||= do {
406 38         104 my $parsed = $self->_parsed;
407              
408 38         154 my %covered = map +( $_ => 1 ), map @{ $_->covered }, values %$parsed;
  50         192  
409              
410 38         688 [ sort keys %covered ];
411             };
412             }
413              
414             sub _trusted_from_pod {
415 37     37   59 my $self = shift;
416              
417 37   66     281 $self->{_trusted_from_pod} ||= do {
418 19         55 my $parsed = $self->_parsed;
419              
420 19         108 [ map @{ $_->trusted }, values %$parsed ];
  22         91  
421             };
422             }
423              
424             sub _private_check {
425 197     197   349 my $self = shift;
426 197         353 my ($sym) = @_;
427 197         270 return scalar grep $sym =~ /$_/, @{ $self->{private} };
  197         5569  
428             }
429              
430             sub _trustme_check {
431 37     37   69 my $self = shift;
432 37         74 my ($sym) = @_;
433              
434             return scalar grep $sym =~ /$_/,
435 37         79 @{ $self->{trustme} },
436 37         53 @{ $self->_trusted_from_pod };
  37         88  
437             }
438              
439             sub _imported_check {
440 200     200   376 my $self = shift;
441 200         351 my ($sym) = @_;
442 200         320 my $package = $self->{package};
443 10     10   92 no strict 'refs';
  10         35  
  10         927  
444 200         305 return !!(B::svref_2object(\*{$package.'::'.$sym})->GvFLAGS & _GVf_IMPORTED_CV);
  200         1605  
445             }
446              
447             1;
448             __END__