File Coverage

blib/lib/PPI/App/ppi_version.pm
Criterion Covered Total %
statement 73 172 42.4
branch 33 90 36.6
condition 3 9 33.3
subroutine 15 22 68.1
pod 0 5 0.0
total 124 298 41.6


line stmt bran cond sub pod time code
1             package PPI::App::ppi_version;
2              
3 2     2   193817 use 5.006;
  2         6  
  2         79  
4 2     2   12 use strict;
  2         3  
  2         59  
5 2     2   11 use warnings;
  2         3  
  2         73  
6 2     2   1704 use version 0.74 ();
  2         4787  
  2         72  
7 2     2   13 use File::Spec 0.80 ();
  2         48  
  2         40  
8 2     2   2848 use Getopt::Long 2.36 ();
  2         25736  
  2         122  
9 2     2   1143 use PPI::Document 1.201 ();
  2         164379  
  2         61  
10 2     2   2298 use File::Find::Rule 0.30 ();
  2         17425  
  2         64  
11 2     2   2004 use File::Find::Rule::Perl 0.03 ();
  2         8285  
  2         56  
12              
13 2     2   25 use vars qw{$VERSION};
  2         4  
  2         88  
14             BEGIN {
15 2     2   3820 $VERSION = '0.14';
16             }
17              
18              
19              
20              
21              
22             #####################################################################
23             # Main Functions
24              
25             sub main {
26 0     0 0 0 my $cmd = shift @_;
27 0 0       0 return usage(@_) unless defined $cmd;
28 0 0       0 return show(@_) if $cmd eq 'show';
29 0 0       0 return change(@_) if $cmd eq 'change';
30 0         0 return error("Unknown command '$cmd'");
31             }
32              
33             sub error {
34 0     0 0 0 my $msg = shift;
35 0         0 chomp $msg;
36 0         0 print "\n";
37 0         0 print " $msg\n";
38 0         0 print "\n";
39 0         0 return 255;
40             }
41              
42              
43              
44              
45              
46             #####################################################################
47             # Command Functions
48              
49             sub usage {
50 0     0 0 0 print "\n";
51 0         0 print "ppi_version $VERSION - Copyright 2006 - 2009 Adam Kennedy.\n";
52 0         0 print "Usage:\n";
53 0         0 print " ppi_version show\n";
54 0         0 print " ppi_version change 0.02_03 0.54\n";
55 0         0 print "\n";
56 0         0 return 0;
57             }
58              
59             sub show {
60             # Find all modules and scripts below the current directory
61 0     0 0 0 my @files = File::Find::Rule->perl_file->in( File::Spec->curdir );
62 0         0 print "Found " . scalar(@files) . " file(s)\n";
63              
64 0         0 my $count = 0;
65 0         0 foreach my $file ( @files ) {
66 0         0 print "$file...";
67 0         0 my $document = PPI::Document->new($file);
68 0 0       0 unless ( $document ) {
69 0         0 print " failed to parse file\n";
70 0         0 next;
71             }
72              
73             # Does the document contain a simple version number
74 0         0 my $elements = $document->find( \&_wanted );
75 0 0       0 unless ( $elements ) {
76 0         0 print " no version\n";
77 0         0 next;
78             }
79 0 0       0 if ( @$elements > 1 ) {
80 0         0 error("$file contains more than one \$VERSION");
81             }
82              
83             # What is that number
84 0         0 my $version = _get_version($elements->[0]);
85 0 0       0 unless ( defined $version ) {
86 0         0 error("Failed to get version string");
87             }
88 0         0 print " $version\n";
89 0         0 $count++;
90             }
91              
92 0         0 print "Found " . scalar($count) . " version(s)\n";
93 0         0 print "Done.\n";
94 0         0 return 0;
95             }
96              
97             sub change {
98 0     0 0 0 my $from = shift @_;
99 0 0 0     0 unless ( $from and $from =~ /^[\d\._]+$/ ) {
100 0         0 error("From is not a number");
101             }
102 0         0 my $to = shift @_;
103 0 0 0     0 unless ( $to and $to =~ /^[\d\._]+$/ ) {
104 0         0 error("To is not a number");
105             }
106              
107             # Find all modules and scripts below the current directory
108 0         0 my @files = File::Find::Rule->perl_file->in( File::Spec->curdir );
109 0         0 print "Found " . scalar(@files) . " file(s)\n";
110              
111 0         0 my $count = 0;
112 0         0 foreach my $file ( @files ) {
113 0         0 print "$file...";
114 0 0       0 if ( ! -w $file ) {
115 0         0 print " no write permission\n";
116 0         0 next;
117             }
118 0         0 my $rv = _change_file( $file, $from => $to );
119 0 0       0 if ( $rv ) {
    0          
120 0         0 print " updated\n";
121 0         0 $count++;
122             } elsif ( defined $rv ) {
123 0         0 print " skipped\n";
124             } else {
125 0         0 print " failed to parse file\n";
126             }
127             }
128              
129 0         0 print "Updated " . scalar($count) . " file(s)\n";
130 0         0 print "Done.\n";
131 0         0 return 0;
132             }
133              
134              
135              
136              
137              
138              
139             #####################################################################
140             # Support Functions
141              
142             sub _change_file {
143 0     0   0 my $file = shift;
144 0         0 my $from = shift;
145 0         0 my $to = shift;
146              
147             # Parse the file
148 0         0 my $document = PPI::Document->new($file);
149 0 0       0 unless ( $document ) {
150 0         0 error("Failed to parse $file");
151             }
152              
153             # Apply the changes
154 0         0 my $rv = _change_document( $document, $from => $to );
155 0 0       0 unless ( defined $rv ) {
156 0         0 error("$file contains more than one \$VERSION assignment");
157             }
158 0 0       0 unless ( $rv ) {
159 0         0 return '';
160             }
161              
162             # Save the updated version
163 0 0       0 unless ( $document->save($file) ) {
164 0         0 error("PPI::Document save failed");
165             }
166              
167 0         0 return 1;
168             }
169              
170             sub _change_document {
171 10     10   4734 my $document = shift;
172 10         16 my $from = shift;
173 10         14 my $to = shift;
174              
175             # Does the document contain an element
176 10         36 my $elements = $document->find( \&_wanted );
177 10 50       116 unless ( $elements ) {
178 0         0 return '';
179             }
180 10 50       22 if ( @$elements > 1 ) {
181 0         0 return undef;
182             }
183              
184             # Find (and if it matches, replace) the version
185 10         20 my $version = _get_version($elements->[0]);
186 10 50       170 unless ( $version eq $from ) {
187 0         0 return '';
188             }
189              
190             # Set the new version
191 10         44 _set_version( $elements->[0], $to );
192              
193 10         36 return 1;
194             }
195              
196             # Extract the version
197             sub _get_version {
198 30     30   7459 my $token = shift;
199 30 100       122 if ( $token->isa('PPI::Token::Quote') ) {
    50          
200 24 100       135 if ( $token->can('literal') ) {
201 12         38 return $token->literal;
202             } else {
203 12         47 return $token->string;
204             }
205             } elsif ( $token->isa('PPI::Token::Number') ) {
206 6 50       24 if ( $token->can('literal') ) {
207 6         17 return $token->literal;
208             } else {
209 0         0 return $token->content;
210             }
211             }
212 0         0 die('Unsupported object ' . ref($token));
213             }
214              
215             # Change the version.
216             # We need to hack some internals to achieve this,
217             # but it will have to do for now.
218             sub _set_version {
219 10     10   12 my $token = shift;
220 10         13 my $to = shift;
221 10 100       137 if ( $token->isa('PPI::Token::Number') ) {
    100          
    100          
    100          
    50          
222 2         4 $token->{content} = $to;
223             } elsif ( $token->isa('PPI::Token::Quote::Single') ) {
224 2         7 $token->{content} = qq|'$to'|;
225             } elsif ( $token->isa('PPI::Token::Quote::Double') ) {
226 2         6 $token->{content} = qq|"$to"|;
227             } elsif ( $token->isa('PPI::Token::Quote::Literal') ) {
228 2         9 substr(
229             $token->{content},
230             $token->{sections}->[0]->{position},
231             $token->{sections}->[0]->{size},
232             $to,
233             );
234             } elsif ( $token->isa('PPI::Token::Quote::Interpolate') ) {
235 2         8 substr(
236             $token->{content},
237             $token->{sections}->[0]->{position},
238             $token->{sections}->[0]->{size},
239             $to,
240             );
241             } else {
242 0         0 die('Unsupported object ' . ref($token));
243             }
244 10         14 return 1;
245             }
246              
247             sub _file_version {
248 0     0   0 my $file = shift;
249 0         0 my $doc = PPI::Document->new($file);
250 0 0       0 unless ( $doc ) {
251 0         0 return "failed to parse file";
252             }
253              
254             # Does the document contain a simple version number
255 0         0 my $elements = $doc->find( \&_find_version );
256 0 0       0 unless ( $elements ) {
257 0         0 return "no version";
258             }
259 0 0       0 if ( @$elements > 1 ) {
260 0         0 error("$file contains more than one \$VERSION");
261             }
262 0         0 my $element = $elements->[0];
263 0         0 my $version = $element->snext_sibling->snext_sibling;
264 0         0 my $version_string = $version->string;
265 0 0       0 unless ( defined $version_string ) {
266 0         0 error("Failed to get version string");
267             }
268              
269 0         0 return version->new($version_string);
270             }
271              
272             # Locate a version number token
273             sub _wanted {
274             # Must be a quote or number
275 350 100 100 350   34768 $_[1]->isa('PPI::Token::Quote') or
276             $_[1]->isa('PPI::Token::Number') or return '';
277              
278             # To the right is a statement terminator or nothing
279 20         93 my $t = $_[1]->snext_sibling;
280 20 50       450 if ( $t ) {
281 20 50       61 $t->isa('PPI::Token::Structure') or return '';
282 20 50       50 $t->content eq ';' or return '';
283             }
284              
285             # To the left is an equals sign
286 20 50       150 my $e = $_[1]->sprevious_sibling or return '';
287 20 50       460 $e->isa('PPI::Token::Operator') or return '';
288 20 50       47 $e->content eq '=' or return '';
289              
290             # To the left is a $VERSION symbol
291 20 50       121 my $v = $e->sprevious_sibling or return '';
292 20 50       424 $v->isa('PPI::Token::Symbol') or return '';
293 20 50       48 $v->content =~ m/^\$(?:\w+::)*VERSION$/ or return '';
294              
295             # To the left is either nothing or "our"
296 20         172 my $o = $v->sprevious_sibling;
297 20 100       326 if ( $o ) {
298 10 50       26 $o->content eq 'our' or return '';
299 10 50       93 $o->sprevious_sibling and return '';
300             }
301              
302 20         169 return 1;
303             }
304              
305             1;