File Coverage

blib/lib/Acme/ReturnValue.pm
Criterion Covered Total %
statement 88 157 56.0
branch 11 48 22.9
condition 2 14 14.2
subroutine 21 27 77.7
pod 6 6 100.0
total 128 252 50.7


line stmt bran cond sub pod time code
1             package Acme::ReturnValue;
2              
3             # ABSTRACT: report interesting return values
4             our $VERSION = '1.004'; # VERSION
5              
6 2     2   325384 use 5.010;
  2         26  
7 2     2   10 use strict;
  2         5  
  2         38  
8 2     2   10 use warnings;
  2         3  
  2         47  
9              
10 2     2   1128 use PPI;
  2         233096  
  2         94  
11 2     2   22 use File::Find;
  2         4  
  2         159  
12 2     2   1088 use Parse::CPAN::Packages;
  2         2688824  
  2         76  
13 2     2   21 use Path::Class qw();
  2         5  
  2         39  
14 2     2   11 use File::Temp qw(tempdir);
  2         5  
  2         112  
15 2     2   13 use File::Path;
  2         6  
  2         103  
16 2     2   12 use File::Copy;
  2         6  
  2         98  
17 2     2   1060 use Archive::Any;
  2         34771  
  2         73  
18 2     2   24 use Data::Dumper;
  2         7  
  2         112  
19 2     2   1477 use JSON;
  2         22280  
  2         15  
20 2     2   279 use Encode;
  2         4  
  2         159  
21 2     2   16 use Moose;
  2         5  
  2         21  
22 2     2   16192 use List::Util qw(any);
  2         8  
  2         150  
23              
24             with qw(MooseX::Getopt);
25 2     2   16 use MooseX::Types::Path::Class;
  2         4  
  2         47  
26              
27             has 'interesting' => (is=>'rw',isa=>'ArrayRef',default=>sub {[]});
28             has 'bad' => (is=>'rw',isa=>'ArrayRef',default=>sub {[]});
29             has 'failed' => (is=>'rw',isa=>'ArrayRef',default=>sub {[]});
30              
31             has 'quiet' => (is=>'ro',isa=>'Bool',default=>0);
32             has 'inc' => (is=>'ro',isa=>'Bool',default=>0);
33             has 'dir' => (is=>'ro',isa=>'Path::Class::Dir',coerce=>1);
34             has 'file' => (is=>'ro',isa=>'Path::Class::File',coerce=>1);
35             has 'cpan' => (is=>'ro',isa=>'Path::Class::Dir',coerce=>1);
36             has 'dump_to' => (is=>'ro',isa=>'Path::Class::Dir',coerce=>1,default=>'returnvalues');
37              
38             has 'json_encoder' => (is=>'ro',lazy_build=>1);
39             sub _build_json_encoder {
40 0     0   0 return JSON->new->pretty;
41             }
42              
43              
44              
45             sub run {
46 0     0 1 0 my $self = shift;
47              
48 0 0       0 if ($self->inc) {
    0          
    0          
    0          
49 0         0 $self->in_INC;
50             }
51             elsif ($self->dir) {
52 0         0 $self->in_dir($self->dir);
53             }
54             elsif ($self->file) {
55 0         0 $self->in_file($self->file);
56             }
57             elsif ($self->cpan) {
58 0         0 $self->in_CPAN($self->cpan,$self->dump_to);
59 0         0 exit;
60             }
61             else {
62 0         0 $self->in_dir('.');
63             }
64              
65 0         0 my $interesting=$self->interesting;
66 0 0       0 if (@$interesting > 0) {
67 0         0 foreach my $cool (@$interesting) {
68 0         0 say $cool->{package} .': '.$cool->{value};
69             }
70             }
71             else {
72 0         0 say "boring!";
73             }
74             }
75              
76              
77             sub waste_some_cycles {
78 8     8 1 47 my ($self, $filename) = @_;
79              
80 8         69 my $doc = PPI::Document->new($filename);
81              
82 8         554642 eval { # I don't care if that fails...
83 8         61 $doc->prune('PPI::Token::Comment');
84 8         109083 $doc->prune('PPI::Token::Pod');
85             };
86              
87 8         106461 my @packages=$doc->find('PPI::Statement::Package');
88 8         112216 my $this_package;
89              
90 8         45 foreach my $node ($packages[0][0]->children) {
91 32 100       203 if ($node->isa('PPI::Token::Word')) {
92 16         57 $this_package = $node->content;
93             }
94             }
95              
96 8         42 my @significant = grep { _is_code($_) } $doc->schildren();
  85         535  
97 8         20 my $match = $significant[-1];
98 8         33 my $rv=$match->content;
99 8         194 $rv=~s/\s*;$//;
100 8         29 $rv=~s/^return //gi;
101              
102 8 100       41 return if $rv eq 1;
103 6 50       22 return if $rv eq '__PACKAGE__';
104 6 100       28 return if $rv =~ /^__PACKAGE__->meta->make_immutable/;
105              
106 5         42 $rv = decode_utf8($rv);
107              
108 5         230 my $data = {
109             'file' => $filename,
110             'package' => $this_package,
111             'PPI' => ref $match,
112             };
113              
114 5         19 my @bad = map { 'PPI::Statement::'.$_} qw(Sub Variable Compound Package Scheduled Include Sub);
  35         75  
115              
116 5 50   35   44 if (any { ref($match) eq $_ } @bad) {
  35 100       93  
117 0         0 $data->{'bad'}=$rv;
118 0         0 push(@{$self->bad},$data);
  0         0  
119             }
120             elsif ($rv =~ /^('|"|\d|qw|qq|q|!|~)/) {
121 4         15 $data->{'value'}=$rv;
122 4         8 push(@{$self->interesting},$data);
  4         141  
123             }
124             else {
125 1         6 $data->{'bad'}=$rv;
126 1         6 $data->{'PPI'}.=" (but very likely crap)";
127 1         4 push(@{$self->bad},$data);
  1         41  
128             }
129             }
130              
131              
132             sub _is_code {
133 85     85   119 my $elem = shift;
134 85   66     454 return ! ( $elem->isa('PPI::Statement::End')
135             || $elem->isa('PPI::Statement::Data'));
136             }
137              
138              
139             sub in_CPAN {
140 0     0 1 0 my ($self,$cpan,$out)=@_;
141              
142 0         0 my $p=Parse::CPAN::Packages->new($cpan->file(qw(modules 02packages.details.txt.gz))->stringify);
143              
144 0 0       0 if (!-d $out) {
145 0 0       0 $out->mkpath || die "cannot make dir $out";
146             }
147              
148             # get all old data files so we can later delete non-current
149 0         0 my %old_files;
150 0         0 while (my $file = $out->next) {
151 0 0       0 next unless $file =~ /\.json/;
152 0         0 $old_files{$file->basename}=1;
153             }
154              
155             # analyse cpan
156 0         0 foreach my $dist (sort {$a->dist cmp $b->dist} $p->latest_distributions) {
  0         0  
157 0         0 delete $old_files{$dist->distvname.'.json'};
158 0 0       0 next if (-e $out->file($dist->distvname.'.json'));
159              
160 0         0 my $data;
161 0         0 my $distfile = $cpan->file('authors','id',$dist->prefix);
162 0         0 $data->{file}=$distfile;
163 0         0 my $dir;
164 0         0 eval {
165 0         0 $dir = tempdir('/var/tmp/arv_XXXXXX');
166 0   0     0 my $archive=Archive::Any->new($distfile->stringify) || die $!;
167 0         0 $archive->extract($dir);
168              
169 0         0 $self->in_dir($dir,$dist->distvname);
170             };
171 0 0       0 if ($@) {
172 0         0 say $@;
173             }
174 0         0 rmtree($dir);
175             }
176              
177             # remove old data files
178 0         0 foreach my $del (keys %old_files) {
179 0 0       0 unlink($out->file($del)) || die $!;
180             }
181              
182             }
183              
184              
185             sub in_INC {
186 0     0 1 0 my $self=shift;
187 0         0 foreach my $dir (@INC) {
188 0         0 $self->in_dir($dir,"INC_$dir");
189             }
190             }
191              
192              
193             sub in_dir {
194 0     0 1 0 my ($self,$dir,$dumpname)=@_;
195 0   0     0 $dumpname ||= $dir;
196 0         0 $dumpname=~s/\//_/g;
197              
198 0 0       0 say $dumpname unless $self->quiet;
199              
200 0         0 $self->interesting([]);
201 0         0 $self->bad([]);
202 0         0 my @pms;
203             find(sub {
204 0 0   0   0 return unless /\.pm\z/;
205 0 0       0 return if $File::Find::name=~/\/x?t\//;
206 0 0       0 return if $File::Find::name=~/\/inc\//;
207 0         0 push(@pms,$File::Find::name);
208 0         0 },$dir);
209              
210 0         0 foreach my $pm (@pms) {
211 0         0 $self->in_file($pm);
212             }
213              
214 0         0 my $dump=Path::Class::Dir->new($self->dump_to)->file($dumpname.".json");
215 0 0 0     0 if ($self->interesting && @{$self->interesting}) {
  0 0 0     0  
216 0         0 $dump->spew(iomode => '>:encoding(UTF-8)', $self->json_encoder->encode($self->interesting));
217             }
218 0         0 elsif ($self->bad && @{$self->bad}) {
219 0         0 $dump->spew(iomode => '>:encoding(UTF-8)', $self->json_encoder->encode($self->bad));
220             }
221             else {
222 0         0 $dump->spew('{"is_boring":"1"}');
223             }
224             }
225              
226              
227             sub in_file {
228 8     8 1 8695 my ($self,$file)=@_;
229              
230 8         22 eval { $self->waste_some_cycles($file) };
  8         33  
231 8 50       27300 if ($@) {
232 0           push (@{$self->failed},{file=>$file,error=>$@});
  0            
233             }
234             }
235              
236             "let's return a strange value from Riga";
237              
238             __END__
239              
240             =pod
241              
242             =encoding UTF-8
243              
244             =head1 NAME
245              
246             Acme::ReturnValue - report interesting return values
247              
248             =head1 VERSION
249              
250             version 1.004
251              
252             =head1 SYNOPSIS
253              
254             use Acme::ReturnValue;
255             my $rvs = Acme::ReturnValue->new;
256             $rvs->in_INC;
257             foreach (@{$rvs->interesting}) {
258             say $_->{package} . ' returns ' . $_->{value};
259             }
260              
261             =head1 DESCRIPTION
262              
263             C<Acme::ReturnValue> will list 'interesting' return values of modules.
264             'Interesting' means something other than '1'.
265              
266             See L<https://returnvalues.plix.at|https://returnvalues.plix.at> for the results of running Acme::ReturnValue on the whole CPAN.
267              
268             =head2 METHODS
269              
270             =head3 run
271              
272             run from the commandline (via F<acme_returnvalue.pl>
273              
274             =head3 waste_some_cycles
275              
276             my $data = $arv->waste_some_cycles( '/some/module.pm' );
277              
278             C<waste_some_cycles> parses the passed in file using PPI. It tries to
279             get the last statement and extract it's value.
280              
281             C<waste_some_cycles> returns a hash with following keys
282              
283             =over
284              
285             =item * file
286              
287             The file
288              
289             =item * package
290              
291             The package defintion (the first one encountered in the file
292              
293             =item * value
294              
295             The return value of that file
296              
297             =back
298              
299             C<waste_some_cycles> will also put this data structure into
300             L<interesting> or L<boring>.
301              
302             You might want to pack calls to C<waste_some_cycles> into an C<eval>
303             because PPI dies on parse errors.
304              
305             =head4 _is_code
306              
307             Stolen directly from Perl::Critic::Policy::Modules::RequireEndWithOne
308             as suggested by Chris Dolan.
309              
310             Thanks!
311              
312             =head3 in_CPAN
313              
314             Analyse CPAN. Needs a local CPAN mirror
315              
316             =head3 in_INC
317              
318             $arv->in_INC;
319              
320             Collect return values from all F<*.pm> files in C<< @INC >>.
321              
322             =head3 in_dir
323              
324             $arv->in_dir( $some_dir );
325              
326             Collect return values from all F<*.pm> files in C<< $dir >>.
327              
328             =head3 in_file
329              
330             $arv->in_file( $some_file );
331              
332             Collect return value from the passed in file.
333              
334             If L<waste_some_cycles> failed, puts information on the failing file into L<failed>.
335              
336             =head3 interesting
337              
338             Returns an ARRAYREF containing 'interesting' modules.
339              
340             =head3 boring
341              
342             Returns an ARRAYREF containing 'boring' modules.
343              
344             =head3 failed
345              
346             Returns an ARRAYREF containing unparsable modules.
347              
348             =head1 BUGS
349              
350             Probably many, because I'm not sure I master PPI yet.
351              
352             =head1 AUTHOR
353              
354             Thomas Klausner <domm@plix.at>
355              
356             =head1 COPYRIGHT AND LICENSE
357              
358             This software is copyright (c) 2013 - 2021 by Thomas Klausner.
359              
360             This is free software; you can redistribute it and/or modify it under
361             the same terms as the Perl 5 programming language system itself.
362              
363             =cut