File Coverage

Refactor.pm
Criterion Covered Total %
statement 181 245 73.8
branch 41 62 66.1
condition 13 18 72.2
subroutine 14 24 58.3
pod 14 14 100.0
total 263 363 72.4


line stmt bran cond sub pod time code
1             # Refactor.pm - refactor Perl code.
2             # $Header: $
3             #
4             ###############################################################################
5             =head1 NAME
6              
7             Devel::Refactor - Perl extension for refactoring Perl code.
8              
9             =head1 VERSION
10              
11             $Revision: $ This is the CVS revision number.
12              
13             =head1 SYNOPSIS
14              
15             use Devel::Refactor;
16            
17             my $refactory = Devel::Refactor->new;
18            
19             my ($new_sub_call,$new_sub_code) =
20             $refactory->extract_subroutine($sub_name, $code_snippet);
21              
22             my $files_to_change = $refactory->rename_subroutine('./path/to/dir',
23             'oldSubName','newSubName');
24             # $files_to_change is a hashref where keys are file names, and values are
25             # arrays of hashes with line_number => new_text
26            
27             =head1 ABSTRACT
28              
29             Perl module that facilitates refactoring Perl code.
30              
31             =head1 DESCRIPTION
32              
33             The B module is for code refactoring.
34              
35             While B may be used from Perl programs, it is also designed to be
36             used with the B plug-in for the B integrated development environment.
37              
38             =cut
39              
40             package Devel::Refactor;
41              
42 6     6   326660 use strict;
  6         15  
  6         251  
43 6     6   30 use warnings;
  6         15  
  6         233  
44 6     6   33 use Cwd;
  6         15  
  6         377  
45 6     6   34 use File::Basename;
  6         19  
  6         21645  
46              
47             require Exporter;
48              
49             our @ISA = qw(Exporter);
50              
51             # Items to export into callers namespace by default. Note: do not export
52             # names by default without a very good reason. Use EXPORT_OK instead.
53             # Do not simply export all your public functions/methods/constants.
54              
55             # This allows declaration use Dev::Refactor ':all';
56             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
57             # will save memory.
58             our %EXPORT_TAGS = ( 'all' => [ qw(
59            
60             ) ] );
61              
62             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
63              
64             our @EXPORT = qw(
65            
66             );
67              
68             our $VERSION = '0.05';
69              
70             our $DEBUG = 0;
71             # Preloaded methods go here.
72              
73              
74             our %perl_file_extensions = (
75             '\.pl$' => 1,
76             '\.pm$' => 1,
77             '\.pod$' => 1,
78             );
79              
80             =head1 CLASS METHODS
81              
82             Just the constructor for now.
83              
84             =head2 new
85              
86             Returns a new B object.
87              
88             =cut
89             # TODO: List the object properties that are initialized.
90              
91             sub new {
92 6     6 1 765 my $class = shift;
93 6         15 $DEBUG = shift;
94              
95             # TODO: Should these really be object properties? No harm I guess, but most
96             # of them are for the extract_subroutine method, and so maybe they
97             # should go in a data structure dedicated to that method?
98 6         159 my $self = {
99             sub_name => '',
100             code_snippet => '',
101             return_snippet => '',
102             return_sub_call => '',
103             eval_err => '',
104             scalar_vars => {},
105             array_vars => {},
106             hash_vars => {},
107             local_scalars => {},
108             loop_scalars => {},
109             local_arrays => {},
110             local_hashes => {},
111             parms => [],
112             inner_retvals => [],
113             outer_retvals => [],
114             perl_file_extensions => { %perl_file_extensions },
115             };
116              
117 6         26 bless $self, $class;
118            
119 6         23 return $self;
120             }
121              
122              
123              
124             =head1 PUBLIC OBJECT METHODS
125              
126             Call on a object returned by new().
127              
128             =head2 extract_subroutine($new_name,$old_code [,$syntax_check])
129              
130             Pass it a snippet of Perl code that belongs in its own subroutine as
131             well as a name for that sub. It figures out which variables
132             need to be passed into the sub, and which variables might be
133             passed back. It then produces the sub along with a call to
134             the sub.
135              
136             Hashes and arrays within the code snippet are converted to
137             hashrefs and arrayrefs.
138              
139             If the I argument is true then a sytax check is performed
140             on the refactored code.
141              
142             Example:
143              
144             $new_name = 'newSub';
145             $old_code = <<'eos';
146             my @results;
147             my %hash;
148             my $date = localtime;
149             $hash{foo} = 'value 1';
150             $hash{bar} = 'value 2';
151             for my $loopvar (@array) {
152             print "Checking $loopvar\n";
153             push @results, $hash{$loopvar} || '';
154             }
155             eos
156              
157             ($new_sub_call,$new_code) = $refactory->extract_subroutine($new_name,$old_code);
158             # $new_sub_call is 'my ($date, $hash, $results) = newSub (\@array);'
159             # $new_code is
160             # sub newSub {
161             # my $array = shift;
162             #
163             # my @results;
164             # my %hash;
165             # my $date = localtime;
166             # $hash{foo} = 'value 1';
167             # $hash{bar} = 'value 2';
168             # for my $loopvar (@$array) {
169             # print "Checking $loopvar\n";
170             # push @results, $hash{$loopvar} || '';
171             # }
172             #
173             #
174             # return ($date, \%hash, \@results);
175             # }
176              
177              
178             Included in the examples directory is a script for use in KDE
179             under Linux. The script gets its code snippet from the KDE
180             clipboard and returns the transformed code the same way. The
181             new sub name is prompted for via STDIN.
182              
183             =cut
184              
185             sub extract_subroutine {
186 2     2 1 11 my $self = shift;
187 2         5 my $sub_name = shift;
188 2         3 my $code_snippet = shift;
189 2         4 my $syntax_check = shift;
190              
191 2 50       8 $DEBUG and print STDERR "sub name : $sub_name\n";
192 2 50       7 $DEBUG and print STDERR "snippet : $code_snippet\n";
193 2         15 $self->{sub_name} = $sub_name;
194 2         5 $self->{code_snippet} = $code_snippet;
195            
196 2         10 $self->_parse_vars();
197 2         7 $self->_parse_local_vars();
198 2         9 $self->_transform_snippet();
199              
200 2 50       6 if ($syntax_check) {
201 0         0 $self->_syntax_check();
202             }
203 2         12 return ( @$self{'return_sub_call','return_snippet'} );
204             }
205              
206             =head2 rename_subroutine($where,$old_name,$new_name,[$max_depth])
207              
208             I is one of:
209             path-to-file
210             path-to-directory
211            
212             If I is a directory then all Perl files (default is C<.pl>, C<.pm>,
213             and C<.pod> See the B method.) in that directory and its'
214             descendents (to I deep,) are searched.
215              
216             Default for I is 0 -- just the directory itself;
217             I of 1 means the specified directory, and it's
218             immeadiate sub-directories; I of 2 means the specified directory,
219             it's sub-directories, and their sub-directrories, and so forth.
220             If you want to scan very deep, use a high number like 99.
221              
222             If no matches are found then returns I, otherwise:
223              
224             Returns a hashref that tells you which files you might want to change,
225             and for each file gives you the line numbers and proposed new text for that line.
226             The hashref looks like this, where I
227             was found on two lines in the first file and on one line in the second file:
228              
229             {
230             ./path/to/file1.pl => [
231             { 11 => "if (myClass->newName($x)) {\n" },
232             { 27 => "my $result = myClass->newName($foo);\n"},
233             ],
234             ./path/to/file2.pm => [
235             { 235 => "sub newName {\n"},
236             ],
237             }
238              
239             The keys are paths to individual files. The values are arraryrefs
240             containing hashrefs where the keys are the line numbers where I
241             was found and the values are the proposed
242             new line, with I changed to I.
243              
244             =cut
245              
246             sub rename_subroutine {
247 6     6 1 39484 my $self = shift;
248 6         13 my $where = shift;
249 6         12 my $old_name = shift;
250 6         10 my $new_name = shift;
251 6   100     38 my $max_depth = shift || 0; # How many level to descend into directories
252              
253 6 50 33     73 return undef unless ($new_name and $old_name);
254              
255 6 50       19 $DEBUG and warn "Looking for $where in ", getcwd(), "\n";
256 6         15 my $found = {}; # hashref of file names
257 6 100       238 if (-f $where){
    50          
258             # it's a file or filehandle
259 2         11 $found->{$where} = $self->_scan_file_for_string ($old_name,$new_name,$where);
260             } elsif ( -d $where ) {
261             # it's a directory or directory handle
262 4         19 $self->_scan_directory_for_string($old_name,$new_name,$where,$found,$max_depth);
263             } else {
264             # uh oh. Should we allow it to be a code snippet?
265 0         0 die "'$where' does not appear to be a file nor a directory."
266             }
267 6 50       33 return %$found ? $found : undef;
268             }
269              
270             =head2 is_perlfile($filename)
271              
272             Takes a filename or path and returns true if the file has one of the
273             extensions in B, otherwise returns false.
274              
275             =cut
276             sub is_perlfile {
277 34     34 1 2795 my ($self,$filename) = @_;
278 34         49 my ($name,$path,$suffix) = fileparse($filename,keys %{$self->perl_file_extensions});
  34         101  
279 34         188 return $suffix;
280             }
281              
282             =head1 OBJECT ACCESSORS
283              
284             These object methods return various data structures that may be stored
285             in a B object. In some cases the method also allows
286             setting the property, e.g. B.
287              
288             =cut
289              
290             =head2 get_new_code
291              
292             Returns the I object property.
293              
294             =cut
295             sub get_new_code{
296 0     0 1 0 my $self = shift;
297            
298 0         0 return $self->{return_snippet};
299             }
300              
301             =head2 get_eval_results
302              
303             Returns the I object property.
304              
305             =cut
306             sub get_eval_results{
307 0     0 1 0 my $self = shift;
308            
309 0         0 return $self->{eval_err};
310             }
311              
312              
313             =head2 get_sub_call
314              
315             Returns the I object property.
316              
317             =cut
318             sub get_sub_call{
319 0     0 1 0 my $self = shift;
320            
321 0         0 return $self->{return_sub_call};
322             }
323              
324              
325             =head2 get_scalars
326              
327             Returns an array of the keys from I object property.
328             =cut
329             sub get_scalars {
330 0     0 1 0 my $self = shift;
331              
332 0         0 return sort keys %{ $self->{scalar_vars} };
  0         0  
333             }
334              
335             =head2 get_arrays
336              
337             Returns an array of the keys from the I object property.
338             =cut
339             sub get_arrays {
340 0     0 1 0 my $self = shift;
341              
342 0         0 return sort keys %{ $self->{array_vars} };
  0         0  
343             }
344              
345             =head2 get_hashes
346              
347             Returns an array of the keys from the I object property.
348              
349             =cut
350             sub get_hashes {
351 0     0 1 0 my $self = shift;
352              
353 0         0 return sort keys %{ $self->{hash_vars} };
  0         0  
354             }
355              
356             =head2 get_local_scalars
357              
358             Returns an array of the keys from the I object property.
359              
360             =cut
361             sub get_local_scalars {
362 0     0 1 0 my $self = shift;
363              
364 0         0 return sort keys %{ $self->{local_scalars} };
  0         0  
365             }
366              
367             =head2 get_local_arrays
368              
369             Returns an array of the keys from the I object property.
370              
371             =cut
372             sub get_local_arrays {
373 0     0 1 0 my $self = shift;
374              
375 0         0 return sort keys %{ $self->{local_arrays} };
  0         0  
376             }
377              
378             =head2 get_local_hashes
379              
380             Returns an array of the keys from the I object property.
381              
382             =cut
383              
384             sub get_local_hashes {
385 0     0 1 0 my $self = shift;
386              
387 0         0 return sort keys %{ $self->{local_hashes} };
  0         0  
388             }
389              
390             =head2 perl_file_extensions([$arrayref|$hashref])
391              
392             Returns a hashref where the keys are regular expressions that match filename
393             extensions that we think are for Perl files. Default are C<.pl>,
394             C<.pm>, and C<.pod>
395              
396             If passed a hashref then it replaces the current values for this object. The
397             keys should be regular expressions, e.g. C<\.cgi$>.
398              
399             If passed an arrayref then the list of values are added as valid Perl
400             filename extensions. The list should be filename extensions, NOT regular expressions,
401             For example:
402              
403             my @additonal_filetypes = qw( .ipl .cgi );
404             my $new_hash = $refactory->perl_file_extensions(\@additional_filetypes);
405             # $new_hash = {
406             # '\.pl$' => 1,
407             # '\.pm$' => 1,
408             # '\.pod$' => 1,
409             # '\.ipl$' => 1,
410             # '\.cgi$' => 1,
411             # '\.t$' => 1,
412             # }
413              
414             =cut
415              
416             sub perl_file_extensions {
417 36     36 1 64 my($self,$args) = @_;
418 36 50       131 if (ref $args eq 'HASH') {
    100          
419 0         0 $self->{perl_file_extensions} = $args;
420             } elsif (ref $args eq 'ARRAY') {
421 2         15 map $self->{perl_file_extensions}->{"\\$_\$"} = 1 , @$args;
422             }
423 36         2051 return $self->{perl_file_extensions};
424             }
425              
426              
427             =head1 TODO LIST
428              
429             =over 2
430              
431             =item Come up with a more uniform approach to B.
432              
433             =item Add more refactoring features, such as I.
434              
435             =item Add a SEE ALSO section with URLs for eclipse/EPIC, refactoring.com, etc.
436              
437             =back
438              
439             =cut
440              
441             ###################################################################################
442             ############################## Utility Methods ####################################
443              
444             sub _parse_vars {
445 2     2   9 my $self = shift;
446              
447 2         4 my $var;
448             my $hint;
449              
450             # find the variables
451 2         24 while ( $self->{code_snippet} =~ /([\$\@]\w+?)(\W\W)/g ) {
452              
453 12         19 $var = $1;
454 12         16 $hint = $2;
455 12 100       52 if ( $hint =~ /^{/ ) { #}/
    50          
    100          
    50          
456 2         8 $var =~ s/\$/\%/;
457 2         13 $self->{hash_vars}->{$var}++;
458             } elsif ( $hint =~ /^\[>/ ) {
459 0         0 $var =~ s/\$/\@/;
460 0         0 $self->{array_vars}->{$var}++;
461             } elsif ( $var =~ /^\@/ ){
462 6         32 $self->{array_vars}->{$var}++;
463             } elsif ( $var =~ /^\%/ ) {
464 0         0 $self->{hash_vars}->{$var}++;
465             } else {
466 4         23 $self->{scalar_vars}->{$var}++;
467             }
468             }
469              
470             }
471              
472             sub _parse_local_vars {
473 2     2   4 my $self = shift;
474              
475 2         3 my $reg;
476             my $reg2;
477 0         0 my $reg3; # To find loops variables declared in for and foreach
478              
479             # figure out which are declared in the snippet
480 2         14 foreach my $var ( keys %{ $self->{scalar_vars} } ) {
  2         10  
481 4         11 $reg = "\\s*my\\s*\\$var\\s*[=;\(]";
482 4         6 $reg2 = "\\s*my\\s*\\(.*?\\$var.*?\\)";
483 4         7 $reg3 = "(?:for|foreach)\\s+my\\s*\\$var\\s*\\(";
484              
485 4 50       234 if ( $var =~ /(?:\$\d+$|\$[ab]$)/ ) {
    50          
486 0         0 $self->{local_scalars}->{$var}++;
487             } elsif ( $self->{code_snippet} =~ /$reg|$reg2/ ) {
488 4         8 $self->{local_scalars}->{$var}++;
489             # skip loop variables
490 4 100       139 if ( $self->{code_snippet} =~ /$reg3/ ) {
491 2         14 $self->{loop_scalars}->{$var}++;
492             }
493             }
494             }
495 2         58 foreach my $var ( keys %{ $self->{array_vars}} ) {
  2         10  
496 4         8 $var =~ s/\$/\@/;
497 4         9 $reg = "\\s*my\\s*\\$var\\s*[=;\(]";
498 4         8 $reg2 = "\\s*my\\s*\\(.*?\\$var.*?\\)";
499              
500 4 100       187 if ( $self->{code_snippet} =~ /$reg|$reg2/ ) {
501 2         9 $self->{local_arrays}->{$var}++;
502             }
503              
504             }
505 2         6 foreach my $var ( keys %{ $self->{hash_vars}} ) {
  2         8  
506 2         5 $var =~ s/\$/\%/;
507 2         4 $reg = "\\s*my\\s*\\$var\\s*[=;\(]";
508 2         5 $reg2 = "\\s*my\\s*\\(.*?\\$var.*?\\)";
509              
510 2 50       60 if ( $self->{code_snippet} =~ /$reg|$reg2/ ) {
511 2         11 $self->{local_hashes}->{$var}++;
512             }
513             }
514              
515             }
516              
517              
518             sub _syntax_check{
519 0     0   0 my $self = shift;
520 0         0 my $tmp;
521            
522 0         0 my $eval_stmt = "my (". join ', ', @{$self->{parms}};
  0         0  
523 0         0 $eval_stmt .= ");\n";
524 0         0 $eval_stmt .= $self->get_sub_call();
525 0         0 $eval_stmt .= $self->get_new_code();
526            
527 0         0 $self->{eval_code} = $eval_stmt;
528            
529 0         0 eval " $eval_stmt ";
530 0 0       0 if ($@) {
531 0         0 $self->{eval_err} = $@;
532            
533 0         0 my @errs = split /\n/, $self->{eval_err};
534 0         0 my @tmp = split /\n/, $self->{return_snippet};
535 0         0 my $line;
536 0         0 foreach my $err (@errs){
537 0 0       0 if ($err =~ /line\s(\d+)/){
538 0         0 $line = ($1 - 3);
539 0         0 $tmp[$line] .= " #<--- ".$err;
540             }
541             }
542 0         0 $self->{return_snippet} = join "\n", @tmp;
543            
544             }
545            
546             }
547              
548             sub _transform_snippet {
549 2     2   4 my $self = shift;
550              
551 2         4 my $reg;
552             my $reg2;
553 0         0 my $arref;
554 0         0 my $href;
555             # Create a sub call that accepts all non-locally declared
556             # vars as parameters
557 2         3 foreach my $parm ( keys %{$self->{scalar_vars} } ) {
  2         11  
558 4 50       24 if ( !defined( $self->{local_scalars}->{$parm} ) ) {
559 0         0 push @{$self->{parms}}, $parm;
  0         0  
560             } else {
561             # Don't return loop variables
562 4 100       27 next if grep $parm eq $_, keys %{$self->{loop_scalars}};
  4         30  
563 2 50       11 if ( $parm !~ /\$\d+$/ ) {
564 2         4 push @{$self->{inner_retvals}}, $parm;
  2         5  
565 2         4 push @{$self->{outer_retvals}}, $parm;
  2         5  
566             }
567             }
568             }
569 2         5 foreach my $parm ( keys %{ $self->{array_vars}} ) {
  2         7  
570 4         7 $parm =~ s/\$/\@/;
571              
572 4 100       12 if ( !defined( $self->{local_arrays}->{$parm} ) ) {
573 2         4 push @{$self->{parms}}, $parm;
  2         6  
574 2         4 $reg2 = "\\$parm";
575 2         6 ($arref = $parm) =~ s/\@/\$/;
576 2         28 $self->{code_snippet} =~ s/$reg2/\@$arref/g;
577            
578 2         7 $parm =~ s/\@/\$/;
579 2         4 $reg = "\\$parm\\[";
580              
581 2         48 $self->{code_snippet} =~ s/$reg/$parm\-\>\[/g;
582              
583            
584             } else {
585 2         4 push @{$self->{inner_retvals}}, "\\$parm"; # \@array
  2         12  
586 2         3 push @{$self->{outer_retvals}}, "$parm";
  2         7  
587             }
588             }
589 2         4 foreach my $parm ( keys %{ $self->{hash_vars} } ) {
  2         7  
590 2         4 $parm =~ s/\$/\%/;
591              
592 2 50       8 if ( !defined( $self->{local_hashes}->{$parm} ) ) {
593 0         0 push @{$self->{parms}}, $parm;
  0         0  
594 0         0 $reg2 = "\\$parm";
595 0         0 ($href = $parm) =~ s/\%/\$/;
596 0         0 $self->{code_snippet} =~ s/$reg2/\%$href/g;
597            
598 0         0 $parm =~ s/\%/\$/;
599 0         0 $reg = "\\$parm\\{";
600              
601 0         0 $self->{code_snippet} =~ s/$reg/$parm\-\>\{/g;
602             } else {
603 2         3 push @{$self->{inner_retvals}}, "\\$parm"; # \%hash
  2         7  
604 2         3 push @{$self->{outer_retvals}}, "$parm";
  2         7  
605             }
606             }
607 2         3 my $retval;
608             my $return_call;
609 0         0 my $tmp;
610            
611 2         4 $return_call .= "my (";
612 2         4 $return_call .= join ', ', map {my $tmp; ($tmp = $_) =~ s/[\@\%](.*)/\$$1/; $tmp} sort @{$self->{outer_retvals}};
  6         6  
  6         25  
  6         26  
  2         15  
613 2         8 $return_call .= ") = ".$self->{sub_name}." (";
614 2         19 $return_call .= join ', ',
615 2         3 map { ( $tmp = $_ ) =~ s/(\%|\@)(.*)/\\$1$2/; $tmp } @{$self->{parms}};
  2         7  
  2         6  
616 2         4 $return_call .= ");\n";
617            
618 2         7 $retval = "sub ".$self->{sub_name}." {\n";
619 2         4 $retval .= join '', map {($tmp = $_) =~ tr/%@/$/; " my $tmp = shift;\n" } @{$self->{parms}};
  2         8  
  2         9  
  2         4  
620 2         9 $retval .= "\n" . $self->{code_snippet};
621 2         12 $retval .= "\n return (";
622 2         5 $retval .= join ', ', sort @{$self->{inner_retvals}};
  2         7  
623 2         4 $retval .= ");\n";
624 2         4 $retval .= "}\n";
625              
626             # protect quotes and dollar signs
627             # $retval =~ s/\"/\\"/g;
628             # $retval =~ s/(\$)/\\$1/g;
629            
630              
631 2         4 $self->{return_snippet} = $retval;
632 2         6 $self->{return_sub_call} = $return_call;
633             }
634              
635              
636             # returns arrayref of hashrefs, or undef
637             sub _scan_file_for_string {
638 12     12   37 my $self = shift;
639 12         18 my $old_name = shift;
640 12         15 my $new_name = shift;
641 12         19 my $file = shift;
642              
643 12         15 my $fh;
644            
645 12   50     526 open $fh, "$file"
646             || die("Could not open code file '$file' - $!");
647              
648 12         24 my $line_number = 0;
649 12         19 my @lines;
650 12         26 my $regex1 = '(\W)(' . $old_name . ')(\W)'; # Surrounded by non-word characters
651 12         25 my $regex2 = "^$old_name(" . '\W)'; # At start of line
652 12         299 while (<$fh>) {
653 304         383 $line_number++;
654             # Look for $old_name surrounded by non-word characters, or at start of line
655 304 100 66     1665 if (/$regex1/o or /$regex2/o) {
656 46         66 my $new_line = $_;
657 46         319 $new_line =~ s/$regex1/$1$new_name$3/g;
658 46         125 $new_line =~ s/$regex2/$new_name$1/;
659 46         141 my $hash = {$line_number => $new_line};
660 46         189 push @lines, $hash;
661             }
662             }
663 12         233 close $fh;
664 12 50       87 return @lines ? \@lines : undef;
665             }
666              
667             # Scan a directory, possibly recuring into sub-directories.
668             sub _scan_directory_for_string {
669 6     6   13 my ($self,$old_name,$new_name,$where,$hash,$depth) = @_;
670 6         10 my $dh;
671 6   50     283 opendir $dh, $where ||
672             die "Could not open directory '$where': $!";
673 6 100       113 my @files = grep { $_ ne '.' and $_ ne '..' } readdir $dh;
  38         165  
674 6         17 close $dh;
675 6         11 $depth--;
676 6         14 foreach my $file (@files) {
677 26         71 $file = "$where/$file"; # add the directory back on to the path
678 26 100 100     381 if (-f $file && $self->is_perlfile($file)) {
679 10         30 $hash->{$file} = $self->_scan_file_for_string($old_name,$new_name,$file);
680             }
681 26 100 100     369 if (-d $file && $depth >= 0) {
682             # It's a directory, so call this method on the directory.
683 2         17 $self->_scan_directory_for_string($old_name,$new_name,$file,$hash,$depth);
684             }
685             }
686 6         91 return $hash;
687             }
688              
689              
690             1; # File must return true when compiled. Keep Perl happy, snuggly and warm.
691              
692             __END__