File Coverage

blib/lib/PPIx/EditorTools/RenameVariable.pm
Criterion Covered Total %
statement 95 105 90.4
branch 36 52 69.2
condition 13 24 54.1
subroutine 11 11 100.0
pod 1 1 100.0
total 156 193 80.8


line stmt bran cond sub pod time code
1             package PPIx::EditorTools::RenameVariable;
2              
3             # ABSTRACT: Lexically replace a variable name in Perl code
4              
5 3     3   480803 use 5.008;
  3         12  
  3         261  
6 3     3   15 use strict;
  3         8  
  3         87  
7 3     3   18 use warnings;
  3         7  
  3         101  
8 3     3   15 use Carp;
  3         4  
  3         178  
9              
10 3     3   15 use base 'PPIx::EditorTools';
  3         7  
  3         1209  
11 3     3   17 use Class::XSAccessor;
  3         10  
  3         20  
12              
13             our $VERSION = '0.18';
14              
15             =pod
16              
17             =head1 NAME
18              
19             PPIx::EditorTools::RenameVariable - Lexically replace a variable name in Perl code
20              
21             =head1 SYNOPSIS
22              
23             my $munged = PPIx::EditorTools::RenameVariable->new->rename(
24             code => $code,
25             line => 15,
26             column => 13,
27             replacement => 'stuff',
28             );
29             my $code_as_strig = $munged->code;
30             my $code_as_ppi = $munged->ppi;
31             my $location = $munged->element->location;
32              
33             =head1 DESCRIPTION
34              
35             This module will lexically replace a variable name.
36              
37             =head1 METHODS
38              
39             =over 4
40              
41             =item new()
42              
43             Constructor. Generally shouldn't be called with any arguments.
44              
45             =item rename( ppi => PPI::Document $ppi, line => Int, column => Int, replacement => Str )
46             =item rename( code => Str $code, line => Int, column => Int, replacement => Str )
47             =item rename( code => Str $code, line => Int, column => Int, to_camel_case => Bool, [ucfirst => Bool] )
48             =item rename( code => Str $code, line => Int, column => Int, from_camel_case => Bool, [ucfirst => Bool] )
49              
50             Accepts either a C to process or a string containing
51             the code (which will be converted into a C) to process.
52             Renames the variable found at line, column with that supplied in the C
53             parameter and returns a C with the
54             new code available via the C or C accessors, as a
55             C or C, respectively. The C found at
56             line, column is available via the C accessor.
57              
58             Instead of specifying an explicit replacement variable name, you may
59             choose to use the C or C options that automatically
60             convert to/from camelCase. In that mode, the C option will force
61             uppercasing of the first letter.
62              
63             You can not specify a replacement name and use the C
64             options.
65              
66             Croaks with a "no token" exception if no token is found at the location.
67             Croaks with a "no declaration" exception if unable to find the declaration.
68              
69             =back
70              
71             =cut
72              
73             sub rename {
74 8     8 1 1106446 my ( $self, %args ) = @_;
75 8         149 $self->process_doc(%args);
76 8   33     46 my $column = $args{column} || croak "column required";
77 8   33     34 my $line = $args{line} || croak "line required";
78 8         26 my $location = [ $line, $column ];
79 8         32 my $replacement = $args{replacement};
80 8 50 100     126 if ( ( $args{to_camel_case} or $args{from_camel_case} )
    50 66        
      66        
81             and defined $replacement )
82             {
83 0         0 croak("Can't accept both replacement name and to_camel_case/from_camel_case");
84             } elsif ( not $args{to_camel_case}
85             and not $args{from_camel_case}
86             and not defined $replacement )
87             {
88 0         0 croak("Need either 'replacement' or to/from_camel_case options");
89             }
90              
91 8         21 my $doc = $self->ppi;
92 8         44 my $token = PPIx::EditorTools::find_token_at_location( $doc, $location );
93              
94 8 50       475 die "no token found" unless defined $token;
95              
96 8         35 my $declaration = PPIx::EditorTools::find_variable_declaration($token);
97 8 50       38 die "no declaration" unless defined $declaration;
98              
99 8         27 $doc->index_locations;
100              
101 8         18108 my $scope = $declaration;
102 8   33     136 while ( not $scope->isa('PPI::Document')
103             and not $scope->isa('PPI::Structure::Block') )
104             {
105 8         50 $scope = $scope->parent;
106             }
107              
108 8         111 my $token_str = $token->content;
109 8         60 my $varname = $token->symbol;
110 8 100       438 if ( not defined $replacement ) {
111 4 100       14 if ( $args{from_camel_case} ) {
112 2         100 $replacement = _from_camel_case( $varname, $args{ucfirst} );
113             } else { # $args{to_camel_case}
114 2         9 $replacement = _to_camel_case( $varname, $args{ucfirst} );
115             }
116 4 50       14 if ( $varname eq $replacement ) {
117 0         0 return PPIx::EditorTools::ReturnObject->new(
118             ppi => $doc,
119             element => $token
120             );
121             }
122             }
123              
124             #warn "VARNAME: $varname";
125              
126             # TODO: This could be part of PPI somehow?
127             # The following string of hacks is simply for finding symbols in quotelikes and regexes
128 8         19 my $type = substr( $varname, 0, 1 );
129 8 100       43 my $brace = $type eq '@' ? '[' : ( $type eq '%' ? '{' : '' );
    50          
130              
131 8         13 my @patterns;
132 8 100 66     53 if ( $type eq '@' or $type eq '%' ) {
133 2         4 my $accessv = $varname;
134 2         38 $accessv =~ s/^\Q$type\E/\$/;
135 2         11 @patterns = (
136             quotemeta( _curlify($varname) ),
137             quotemeta($varname),
138             quotemeta($accessv) . '(?=' . quotemeta($brace) . ')',
139             );
140 2 50       11 if ( $type eq '%' ) {
    0          
141 2         11 my $slicev = $varname;
142 2         10 $slicev =~ s/^\%/\@/;
143 2         30 push @patterns, quotemeta($slicev) . '(?=' . quotemeta($brace) . ')';
144             } elsif ( $type eq '@' ) {
145 0         0 my $indexv = $varname;
146 0         0 $indexv =~ s/^\@/\$\#/;
147 0         0 push @patterns, quotemeta($indexv);
148             }
149             } else {
150 6         26 @patterns = (
151             quotemeta( _curlify($varname) ),
152             quotemeta($varname) . "(?![\[\{])"
153             );
154             }
155 8         19 my %unique;
156 8         20 my $finder_regexp = '(?:' . join( '|', grep { !$unique{$_}++ } @patterns ) . ')';
  20         93  
157              
158 8         337 $finder_regexp = qr/$finder_regexp/; # used to find symbols in quotelikes and regexes
159             #warn $finder_regexp;
160              
161 8         33 $replacement =~ s/^\W+//;
162              
163             $scope->find(
164             sub {
165 468     468   5463 my $node = $_[1];
166 468 100 33     3323 if ( $node->isa("PPI::Token::Symbol") ) {
    50          
    100          
167 44 100       99 return 0 unless $node->symbol eq $varname;
168              
169             # TODO do this without breaking encapsulation!
170 18         695 $node->{content} = substr( $node->content(), 0, 1 ) . $replacement;
171             }
172              
173             # This used to be a simple "if". Patrickas: "[elsif] resolves this
174             # issue but it may introduce other bugs since I am not sure I
175             # understand the code that follows it."
176             # See Padre trac ticket #655 for the full comment. Remove this
177             # comment if there are new bugs resulting from this change.
178             elsif ( $type eq '@' and $node->isa("PPI::Token::ArrayIndex") ) { # $#foo
179 0 0       0 return 0
180             unless substr( $node->content, 2 ) eq substr( $varname, 1 );
181              
182             # TODO do this without breaking encapsulation!
183 0         0 $node->{content} = '$#' . $replacement;
184             } elsif ( $node->isa("PPI::Token") ) { # the case of potential quotelikes and regexes
185 360         791 my $str = $node->content;
186 360 100       2448 if ($str =~ s{($finder_regexp)([\[\{]?)}<
187 4 50       17 if ($1 =~ tr/{//) { substr($1, 0, ($1=~tr/#//)+1) . "{$replacement}$2" }
  4         28  
188 0         0 else { substr($1, 0, ($1=~tr/#//)+1) . "$replacement$2" }
189             >ge
190             )
191             {
192              
193             # TODO do this without breaking encapsulation!
194 4         11 $node->{content} = $str;
195             }
196             }
197 442         1053 return 0;
198             },
199 8         105 );
200              
201 8         216 return PPIx::EditorTools::ReturnObject->new(
202             ppi => $doc,
203             element => $token,
204             );
205             }
206              
207             # converts a variable name to camel case and optionally converts the
208             # first character to upper case
209             sub _to_camel_case {
210 38     38   4184 my $var = shift;
211 38         69 my $ucfirst = shift;
212 38         40 my $prefix;
213 38 100       157 if ( $var =~ s/^(\W*_)// ) {
214 6         11 $prefix = $1;
215             }
216 38         249 $var =~ s/_([[:alpha:]])/\u$1/g;
217 38 100       172 $var =~ s/^([^[:alpha:]]*)([[:alpha:]])/$1\u$2/ if $ucfirst;
218 38 100       76 $var = $prefix . $var if defined $prefix;
219 38         188 return $var;
220             }
221              
222             sub _from_camel_case {
223 56     56   6569 my $var = shift;
224 56         77 my $ucfirst = shift;
225 56         63 my $prefix;
226 56 50       277 if ( $var =~ s/^(\W*_?)// ) {
227 56         111 $prefix = $1;
228             }
229 56 100       111 if ($ucfirst) {
230 28         50 $var = lcfirst($var);
231 28         158 $var =~ s/([[:upper:]])/_\u$1/g;
232 28         152 $var =~ s/^([^[:alpha:]]*)([[:alpha:]])/$1\u$2/;
233             } else {
234 28         168 $var =~ s/^([^[:alpha:]]*)([[:alpha:]])/$1\l$2/;
235 28         144 $var =~ s/([[:upper:]])/_\l$1/g;
236             }
237 56 50       156 $var = $prefix . $var if defined $prefix;
238 56         278 return $var;
239             }
240              
241              
242             sub _curlify {
243 8     8   13 my $var = shift;
244 8 50       117 if ( $var =~ s/^([\$\@\%])(.+)$/${1}{$2}/ ) {
245 8         54 return ($var);
246             }
247 0           return ();
248             }
249              
250             1;
251              
252             __END__