File Coverage

blib/lib/PPIx/EditorTools/RenameVariable.pm
Criterion Covered Total %
statement 93 103 90.2
branch 36 52 69.2
condition 14 24 58.3
subroutine 11 11 100.0
pod 1 1 100.0
total 155 191 81.1


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