File Coverage

blib/lib/PPIx/EditorTools/IntroduceTemporaryVariable.pm
Criterion Covered Total %
statement 61 61 100.0
branch 20 26 76.9
condition 6 9 66.6
subroutine 8 8 100.0
pod 0 1 0.0
total 95 105 90.4


line stmt bran cond sub pod time code
1             package PPIx::EditorTools::IntroduceTemporaryVariable;
2              
3             # ABSTRACT: Introduces a temporary variable using PPI
4              
5 2     2   261061 use 5.008;
  2         8  
  2         79  
6 2     2   11 use strict;
  2         3  
  2         63  
7 2     2   11 use warnings;
  2         6  
  2         63  
8 2     2   10 use Carp;
  2         3  
  2         145  
9              
10 2     2   8 use base 'PPIx::EditorTools';
  2         4  
  2         698  
11 2         27 use Class::XSAccessor accessors => {
12             'start_location' => 'start_location',
13             'end_location' => 'end_location',
14             'expression' => 'expression',
15             'location' => 'location',
16 2     2   13 };
  2         4  
17              
18             our $VERSION = '0.18';
19              
20             =pod
21              
22             =head1 NAME
23              
24             PPIx::EditorTools::IntroduceTemporaryVariable - Introduces a temporary variable using PPI
25              
26             =head1 SYNOPSIS
27              
28             my $munged = PPIx::EditorTools::IntroduceTemporaryVariable->new->introduce(
29             code => "use strict; BEGIN {
30             $^W = 1;
31             }\n\tmy $x = ( 1 + 10 / 12 ) * 2;\n\tmy $y = ( 3 + 10 / 12 ) * 2;\n",
32             start_location => [ 2, 19 ],
33             end_location => [ 2, 25 ],
34             varname => '$foo',
35             );
36             my $modified_code_as_string = $munged->code;
37             my $location_of_new_var_declaration = $munged->element->location;
38              
39             =head1 DESCRIPTION
40              
41             Given a region of code within a statement, replaces all occurrences of
42             that code with a temporary variable. Declares and initializes the
43             temporary variable right above the statement that included the
44             selected expression.
45              
46             =head1 METHODS
47              
48             =over 4
49              
50             =item new()
51              
52             Constructor. Generally shouldn't be called with any arguments.
53              
54             =item find( ppi => PPI::Document, start_location => Int, end_location => Int, varname => Str )
55             =item find( code => Str, start_location => Int, end_location => Int, varname => Str )
56              
57             Accepts either a C to process or a string containing
58             the code (which will be converted into a C) to process.
59              
60             Given the region of code specified by start_location and end_location,
61             replaces that code with a temporary variable with the name given
62             in varname (defaults to C). Declares and initializes
63             the temporary variable right above the statement that included the
64             selected expression.
65              
66             Returns a C with the modified code
67             as a string available via the C accessor (or as a C
68             via the C accessor), and the C where the new variable
69             is declared available via the C accessor.
70              
71             Croaks with a "no token" exception if no token is found at the location.
72             Croaks with a "no statement" exception if unable to find the statement.
73              
74             =back
75              
76             =cut
77              
78             sub introduce {
79 4     4 0 728 my ( $self, %args ) = @_;
80 4         58 $self->process_doc(%args);
81 4 50       24 my $start_loc = $args{start_location} or croak "start_location required";
82 4 50       19 my $end_loc = $args{end_location} or croak "end_location required";
83 4         12 my $varname = $args{varname};
84 4 100       14 $varname = 'tmp' if not defined $varname;
85 4 100       20 $varname = '$' . $varname if $varname !~ /^[\$\@\%]/;
86              
87 4         13 my $ppi = $self->ppi;
88 4         21 $ppi->flush_locations;
89              
90 4         1513 my $token = PPIx::EditorTools::find_token_at_location( $ppi, $start_loc );
91 4         257 $ppi->flush_locations;
92 4 50       1300 die "no token" unless $token;
93              
94 4         31 my $statement = $token->statement();
95 4 50       136 die "no statement" unless $statement;
96              
97             # walk up the PPI tree until we reach a sort of structure that's not a statement.
98             # FIXME: This may or may not be robust. A PPI::Statement claims to be what's
99             # defined as "statements" in perlsyn, but it's not! perlsyn says all statements
100             # end in a semicolon unless at the end of a block.
101             # For PPI, Statements can be part of others and thus don't necessarily have
102             # a semicolon.
103 4         8 while (1) {
104 6         29 my $parent = $statement->statement();
105 6 50       69 last if not defined $parent;
106 6 50       25 if ( $parent eq $statement ) { # exactly the same object, ie. is a statement already
107 6         1387 $parent = $statement->parent(); # force the parent
108             last
109 6 100 66     106 if not $parent # stop if we're at a block or at the document level
      66        
110             or $parent->isa('PPI::Structure::Block')
111             or $parent->isa('PPI::Structure::Document');
112 5         96 $parent = $parent->statement(); # force it to be a statement
113             }
114 5 100 66     114 last if not $parent # stop if the parent isn't a statement
115             or not $parent->isa('PPI::Statement');
116 2         5 $statement = $parent;
117             }
118 4         22 my $location_for_insert = $statement->location;
119 4         8214 $self->location($location_for_insert);
120              
121             # TODO: split on a look behind \n so we keep the \n
122 4         26 my @code = map {"$_\n"} split( /\n/, $ppi->serialize );
  18         3634  
123              
124 4         12 my $expr;
125 4         17 for my $line_num ( $start_loc->[0] .. $end_loc->[0] ) {
126 5         15 my $line = $code[ $line_num - 1 ]; # 0 based index to 1 base line numbers
127              
128 5 100       22 substr( $line, $end_loc->[1] ) = '' if $line_num == $end_loc->[0];
129 5 100       21 substr( $line, 0, $start_loc->[1] - 1 ) = ''
130             if $line_num == $start_loc->[0];
131              
132 5         16 $expr .= $line;
133             }
134              
135 4         19 $self->expression($expr);
136              
137 4         8 my $indent = '';
138 4 100       41 $indent = $1 if $code[ $location_for_insert->[0] - 1 ] =~ /^(\s+)/;
139              
140 4         8 my $place_holder = 'XXXXX_PPIx_EDITOR_PLACE_HOLDER_XXXXX';
141 4         44 substr(
142             $code[ $location_for_insert->[0] - 1 ],
143             $location_for_insert->[1] - 1, 0
144             ) = sprintf( "my %s = %s;\n%s", $varname, $place_holder, $indent );
145              
146             # TODO: need to watch for word boundries etc...
147 4         25 my $code = join( '', @code );
148 4         141 $code =~ s/\Q$expr\E/$varname/gm;
149 4         42 $code =~ s/\Q$place_holder\E/$expr/gm;
150              
151             return PPIx::EditorTools::ReturnObject->new(
152             code => $code,
153             element => sub {
154 2     2   9 PPIx::EditorTools::find_token_at_location(
155             shift->ppi,
156             $location_for_insert
157             );
158             }
159 4         57 );
160             }
161              
162             1;
163              
164             __END__