File Coverage

blib/lib/Acme/ReturnValue.pm
Criterion Covered Total %
statement 84 153 54.9
branch 11 48 22.9
condition 2 14 14.2
subroutine 19 25 76.0
pod 6 6 100.0
total 122 246 49.5


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