File Coverage

blib/lib/Ref/Util/Rewriter.pm
Criterion Covered Total %
statement 115 127 90.5
branch 38 56 67.8
condition 8 11 72.7
subroutine 14 14 100.0
pod 3 3 100.0
total 178 211 84.3


line stmt bran cond sub pod time code
1             package Ref::Util::Rewriter;
2             # ABSTRACT: Rewrite your code to use Ref::Util
3              
4 2     2   172465 use strict;
  2         19  
  2         58  
5 2     2   11 use warnings;
  2         3  
  2         51  
6              
7 2     2   1054 use PPI;
  2         214247  
  2         83  
8 2     2   880 use Safe::Isa;
  2         918  
  2         240  
9 2     2   15 use Exporter qw< import >;
  2         4  
  2         55  
10 2     2   11 use List::Util qw< first >;
  2         6  
  2         3002  
11              
12             our @EXPORT_OK = qw< rewrite_string rewrite_file >;
13              
14             my %reftype_to_reffunc = (
15             SCALAR => 'is_scalarref',
16             ARRAY => 'is_arrayref',
17             HASH => 'is_hashref',
18             CODE => 'is_coderef',
19             Regexp => 'is_regexpref',
20             GLOB => 'is_globref',
21             IO => 'is_ioref',
22             REF => 'is_refref',
23             );
24              
25             sub rewrite_string {
26 17     17 1 9315 my $string = shift;
27 17         81 my $res = rewrite_doc( PPI::Document->new(\$string) );
28 17         1870 return $res;
29             }
30              
31             sub rewrite_file {
32 1     1 1 975 my $file = shift;
33 1         12 my $content = rewrite_doc( PPI::Document->new($file) );
34              
35 1 50       255 open my $fh, '>', $file
36             or die "Failed to open file $file: $!";
37 1         401 print {$fh} $content;
  1         25  
38 1         127 close $fh;
39              
40 1         15 return $content;
41             }
42              
43             sub rewrite_doc {
44 18 50   18 1 37868 my $doc = shift or die;
45 18         65 my $all_statements = $doc->find('PPI::Statement');
46 18 50       10361 $all_statements = [] unless defined $all_statements;
47              
48 18         60 my @cond_ops = qw;
49 18         25 my @new_statements;
50              
51             ALL_STATEMENTS:
52 34         563 foreach my $statement ( @{$all_statements} ) {
  34         157  
53             # if there's an "if()" statement, it appears as a Compound statement
54             # and then each internal statement appears again,
55             # causing duplication in results
56 59 100       440 $statement->$_isa('PPI::Statement::Compound')
57             and next;
58              
59 57         972 _handle_eval($statement);
60              
61             # find the 'ref' functions
62             my $ref_subs = $statement->find( sub {
63 521 100   521   6844 $_[1]->isa('PPI::Token::Word') && $_[1]->content eq 'ref'
64 57 100       221 }) or next;
65              
66 16         196 my $statement_def;
67              
68             REF_STATEMENT:
69 16         27 foreach my $ref_sub ( @{$ref_subs} ) {
  16         38  
70             # we want to pick up everything until we find a delimiter
71             # effectively telling us we ended the parameters to "ref"
72 16         22 my $sib = $ref_sub;
73 16         40 my ( @func_args, $reffunc_doc, @rest_of_tokens );
74              
75 16         0 my @siblings_to_remove;
76              
77 16         55 while ( $sib = $sib->next_sibling ) {
78             # end of statement/expression
79 89         2453 my $content = $sib->content;
80              
81 89         477 push @siblings_to_remove, $sib;
82 89 100       181 $content eq ';' and last;
83              
84             # we might already have a statement
85             # in this case collect all the rest of the tokens
86             # (this could be in two separate loops)
87 79 100       135 if ($statement_def) {
88 20         30 push @rest_of_tokens, $sib;
89 20         48 next;
90             }
91              
92             # reasons to stop
93 59 100 66     181 if ( ! $statement_def && $sib->$_isa('PPI::Token::Operator') ) {
94             # comparison operators
95 16 100 66     223 if ( $content eq 'eq' || $content eq 'ne' ) {
    50          
96             # "ARRAY" vs. $foo (which has "ARRAY" as value)
97             # we also move $sib to next significant sibling
98 15         52 my $val_token = $sib = $sib->snext_sibling;
99 15 50       433 my $val_str = $val_token->$_isa('PPI::Token::Quote')
100             ? $val_token->string
101             : $val_token->content;
102              
103 15         313 my $func = $reftype_to_reffunc{$val_str};
104 15 50       39 if ( !$func ) {
105 0         0 warn "Error: no match for $val_str\n";
106 0         0 next REF_STATEMENT;
107             }
108              
109 15         54 push @siblings_to_remove, $sib;
110              
111 15         66 $statement_def = [ $func, \@func_args, '' ];
112 1     1   5 } elsif ( first { $content eq $_ } @cond_ops ) {
113             # is_ref
114              
115             # @func_args will now contain spaces too,
116             # which we will need to take out,
117             # in order to add them after the is_ref()
118             # reason those spaces don't appear in is_ref()
119             # we created is because we clean the function up
120 1         3 my $spaces_count = 0;
121 1         3 foreach my $idx ( reverse 0 .. $#func_args ) {
122 2 100       18 $func_args[$idx]->$_isa('PPI::Token::Whitespace')
123             ? $spaces_count++
124             : last;
125             }
126              
127             # we should add these *and* the cond op
128             # to the statement
129             # technically we can just add them at the end
130             # but it seems easier to stick them as strings
131             # and have them parsed
132             # (wish i understood PPI better)
133              
134             $statement_def = [
135 1         19 'is_ref',
136             \@func_args,
137             ' ' x $spaces_count . $content,
138             ];
139             } else {
140 0         0 warn "Warning: unknown operator: $sib\n";
141 0         0 next REF_STATEMENT;
142             }
143             } else {
144             # otherwise, collect it as a parameter
145 43         715 push @func_args, $sib;
146             }
147             }
148              
149             # skip when failed (error or warnings should appear from above)
150 16 50       159 $statement_def or next;
151              
152 16         23 my ( $func_name, $func_args, $rest ) = @{$statement_def};
  16         45  
153 16         40 $rest .= $_ for @rest_of_tokens;
154 16 100 66     101 $sib && $sib->content eq ';'
155             and $rest .= ';';
156              
157 16         89 $reffunc_doc = _create_statement(
158             $func_name, $func_args, $rest
159             );
160              
161             # prevent garbage collection
162             # FIXME: turn this into an interation that finds weaken
163             # objects and unweakens them (Scalar::Util::unweaken)
164 16         21109 push @new_statements, $reffunc_doc;
165              
166 16         58 my $new_statement = ( $reffunc_doc->children )[0];
167              
168             # remove as much as we can
169 16         104 foreach my $element ( @siblings_to_remove ) {
170 104         3048 $element->remove;
171             }
172              
173             # remove the trailing space to avoid duplicate spaces
174 16 100       497 if ( my $next = $ref_sub->next_sibling ) {
175 15 50       325 $next->remove if $next->isa('PPI::Token::Whitespace');
176             }
177              
178 16         503 foreach my $element ( $new_statement->children ) {
179 64         2119 my $insert = $ref_sub->insert_before( $element );
180             }
181              
182 16         635 $ref_sub->remove;
183              
184             # update statements... to avoid PPI issues when moving elements...
185             # this is very ugly... but probably the best solution
186 16         577 $all_statements = $doc->find('PPI::Statement');
187 16         9712 goto ALL_STATEMENTS;
188             }
189             }
190              
191 18         238 return "$doc";
192             }
193              
194              
195             sub _handle_eval {
196 57     57   88 my $statement = shift;
197             my $evals = $statement->find( sub {
198 521     521   7134 $_[1]->isa('PPI::Token::Word') && $_[1]->content eq 'eval';
199 57   100     254 }) || [];
200              
201 57         780 foreach my $eval ( @{$evals} ) {
  57         130  
202 3         6 my $sib = $eval;
203              
204 3         9 while ( $sib = $sib->next_sibling ) {
205 6 100       126 if ( $sib->isa('PPI::Token::Quote') ) {
206 3 50       10 last unless $sib->content =~ qr{\bref\b}; # shortcut
207              
208             # '' - PPI::Token::Quote::Single
209             # "q{}" - PPI::Token::Quote::Literal
210             # "" - PPI::Token::Quote::Double
211             # "qq{}" - PPI::Token::Quote::Interpolate
212 3         35 my ( $content, $after, $before );
213              
214 3 50       29 if ( $sib->isa('PPI::Token::Quote::Single') ) {
    100          
    50          
    0          
215 0         0 $after = $before = q{'};
216 0         0 $content = $sib->literal;
217             } elsif ( $sib->isa('PPI::Token::Quote::Double') ) {
218 1         3 $after = $before = q{"};
219 1         6 $content = $sib->string;
220             } elsif ( $sib->isa('PPI::Token::Quote::Literal') ) {
221 2         6 $before = 'q{';
222 2         4 $after = '}';
223              
224 2 50       5 if ( $sib->content =~ qr{^q(.).*(.)$} ) {
225 2         24 $before = 'q' . $1;
226 2         4 $after = $2;
227             }
228              
229 2         10 $content = $sib->literal;
230             } elsif ( $sib->isa('PPI::Token::Quote::Interpolate') ) {
231 0         0 $before = 'qq{';
232 0         0 $after = '}';
233              
234 0 0       0 if ( $sib->string =~ qr{^q(.).*(.)$} ) {
235 0         0 $before = $1;
236 0         0 $after = $2;
237             }
238              
239 0         0 $content = $sib->string;
240             }
241              
242             $content
243 3 50       60 or next;
244              
245             # FIXME: this is very ugly....
246             # FIXME need to escape for literals but the idea is there
247 3         8 $sib->{'content'}
248             = $before . rewrite_string($content) . $after;
249              
250             # Idea:
251             # my $p = PPI::Token::Quote::Double->new( rewrite_string($content) );
252             # $sib->insert_before( $p );
253             # $sib->delete;
254 3         113 last;
255             }
256             }
257              
258             }
259             }
260              
261             sub _create_statement {
262 16     16   37 my ( $func, $args, $rest ) = @_;
263 16         25 my $args_str = join '', @{$args};
  16         57  
264 16         293 $args_str =~ s/^\s+//;
265 16         81 $args_str =~ s/\s+$//;
266 16         39 $args_str =~ s/^\(+//;
267 16         40 $args_str =~ s/\)+$//;
268 16 50       36 defined $rest or $rest = '';
269 16         94 return PPI::Document::Fragment->new(\"$func($args_str)$rest");
270             }
271              
272             1;
273              
274             __END__