File Coverage

lib/B/DeparseTree/SyntaxTree.pm
Criterion Covered Total %
statement 174 219 79.4
branch 93 138 67.3
condition 26 50 52.0
subroutine 16 16 100.0
pod 0 14 0.0
total 309 437 70.7


line stmt bran cond sub pod time code
1             # B::DeparseTree tree-building routines.
2             # Copyright (c) 2018 Rocky Bernstein
3             # All rights reserved.
4             # This module is free software; you can redistribute and/or modify
5             # it under the same terms as Perl itself.
6              
7             # This is based on the module B::Deparse by Stephen McCamant.
8             # It has been extended save tree structure, and is addressible
9             # by opcode address.
10              
11             # Note the package name. It is *not* B::DeparseTree::Tree.
12             # In the future there may be a version of this that doesn't
13             # save as much information, but just stores enough to extract
14             # a string, which would be a slightly more heavyweight version of
15             # B::Deparse.
16             package B::DeparseTree::SyntaxTree;
17              
18 3     3   1119 use B::DeparseTree::Node;
  3         6  
  3         4265  
19              
20             our($VERSION, @EXPORT, @ISA);
21             $VERSION = '3.2.0';
22             @ISA = qw(Exporter B::DeparseTree);
23             @EXPORT = qw(
24             combine
25             combine2str
26             get_info_and_str
27             expand_simple_spec
28             indent_less
29             indent_more
30             indent_value
31             info2str
32             info_from_list
33             info_from_template
34             info_from_string
35             info_from_text
36             template_engine
37             template2str
38             );
39              
40             sub combine($$$)
41             {
42 593     593 0 1576 my ($self, $sep, $items) = @_;
43             # FIXME: loop over $item, testing type.
44 593 50       1542 Carp::confess("should be a reference to a array: is $items") unless
45             ref $items eq 'ARRAY';
46 593         958 my @result = ();
47 593         1473 foreach my $item (@$items) {
48 764         981 my $add;
49 764 50       1338 if (ref $item) {
50 764 50 33     1718 if (ref $item eq 'ARRAY' and scalar(@$item) == 2) {
    50          
51 0         0 $add = [$item->[0], $item->[1]];
52 764         2238 } elsif (eval{$item->isa("B::DeparseTree::Node")}) {
53 764         1863 $add = [$item->{text}, $item->{addr}];
54             # First item is text and second item is op address.
55             } else {
56 0         0 Carp::confess("don't know what to do with $item");
57             }
58             } else {
59 0         0 $add = $item;
60             }
61 764 100 66     2564 push @result, $sep if @result && $sep;
62 764         1356 push @result, $add;
63             }
64 593         2476 return @result;
65             }
66              
67             sub combine2str($$$)
68             {
69 4501     4501 0 7251 my ($self, $sep, $items) = @_;
70 4501         5287 my $result = '';
71 4501         6950 foreach my $item (@$items) {
72 21697 100       30763 $result .= $sep if $result;
73 21697 100       30303 if (ref $item) {
74 9599 100 66     21206 if (ref $item eq 'ARRAY' and scalar(@$item) == 2) {
    50          
75             # First item is text and second item is op address.
76 5351         8417 $result .= $self->info2str($item->[0]);
77 4248         9460 } elsif (eval{$item->isa("B::DeparseTree::Node")}) {
78 4248 50       6163 if (exists $item->{fmt}) {
79 4248         6782 $result .= $self->template2str($item);
80             } else {
81 0         0 $result .= $self->info2str($item);
82             }
83             } else {
84 0         0 Carp::confess("Invalid ref item ref($item)");
85             }
86             } else {
87             # FIXME: add this and remove errors
88 12098 50       18283 if (index($item, '@B::DeparseTree::Node') > 0) {
89 0         0 Carp::confess("\@B::DeparseTree::Node as an item is probably wrong");
90             }
91 12098         16117 $result .= $item;
92             }
93             }
94 4501         8105 return $result;
95             }
96              
97             sub expand_simple_spec($$)
98             {
99 33139     33139 0 46227 my ($self, $fmt) = @_;
100 33139         37418 my $result = '';
101 33139         53467 while ((my $k=index($fmt, '%')) >= 0) {
102 26473         34040 $result .= substr($fmt, 0, $k);
103 26473         30766 my $spec = substr($fmt, $k, 2);
104 26473         33303 $fmt = substr($fmt, $k+2);
105              
106 26473 50       54053 if ($spec eq '%%') {
    100          
    100          
    50          
107 0         0 $result .= '%';
108             } elsif ($spec eq '%+') {
109 2770         4741 $result .= $self->indent_more();
110             } elsif ($spec eq '%-') {
111 2770         6068 $result .= $self->indent_less();
112             } elsif ($spec eq '%|') {
113 20933         30336 $result .= $self->indent_value();
114             } else {
115 0         0 Carp::confess("Unknown spec $spec")
116             }
117             }
118 33139 100       50153 $result .= $fmt if $fmt;
119 33139         55008 return $result;
120             }
121              
122             sub indent_less($$) {
123 2770     2770 0 4289 my ($self, $check_level) = @_;
124 2770 50       5025 $check_level = 0 if !defined $check_level;
125              
126 2770         4204 $self->{level} -= $self->{'indent_size'};
127 2770         3437 my $level = $self->{level};
128 2770 50       4261 if ($check_level < 0) {
129 0 0       0 Carp::confess("mismatched indent/dedent") if $check_level;
130 0         0 $level = 0;
131 0         0 $self->{level} = 0;
132             }
133 2770         4711 return $self->indent_value();
134             }
135              
136             sub indent_more($) {
137 2770     2770 0 3706 my ($self) = @_;
138 2770         4039 $self->{level} += $self->{'indent_size'};
139 2770         3903 return $self->indent_value();
140             }
141              
142             sub indent_value($) {
143 26473     26473 0 32552 my ($self) = @_;
144 26473         31251 my $level = $self->{level};
145 26473 50       37779 if ($self->{'use_tabs'}) {
146 0         0 return "\t" x ($level / 8) . " " x ($level % 8);
147             } else {
148 26473         82772 return " " x $level;
149             }
150             }
151              
152             sub info2str($$)
153             {
154 111202     111202 0 139303 my ($self, $item) = @_;
155 111202         125456 my $result = '';
156 111202 100       155423 if (ref $item) {
157 97881 50 33     170644 if (ref $item eq 'ARRAY' and scalar(@$item) == 2) {
    50          
158             # This code is going away...
159 0         0 Carp::confess("fixme");
160 0         0 $result = $item->[0];
161 97881         211993 } elsif (eval{$item->isa("B::DeparseTree::Node")}) {
162 97881 100 66     190870 if (exists $item->{fmt}) {
    100          
163 47881         71426 $result .= $self->template2str($item);
164 47881 100       85219 if ($item->{maybe_parens}) {
165 7256         9211 my $mp = $item->{maybe_parens};
166 7256 100 66     20413 if ($mp->{force} || $mp->{parens}) {
167 192         367 $result = "($result)";
168             }
169             }
170             } elsif (!exists $item->{texts} && exists $item->{text}) {
171             # Is a constant string
172 46094         65039 $result .= $item->{text};
173             } else {
174             $result = $self->combine2str($item->{sep},
175 3906         6972 $item->{texts});
176             }
177              
178             } else {
179 0         0 Carp::confess("Invalid ref item ref($item)");
180             }
181             } else {
182             # FIXME: add this and remove errors
183 13321 50       23266 if (index($item, '@B::DeparseTree::Node') > 0) {
184 0         0 Carp::confess("\@B::DeparseTree::Node as an item is probably wrong");
185             }
186 13321         15751 $result = $item;
187             }
188 111202         182272 return $result;
189             }
190              
191             # Create an info structure from a list of strings
192             # FIXME: $deparse (or rather $self) should be first
193             sub info_from_list($$$$$$)
194             {
195 46     46 0 133 my ($op, $self, $texts, $sep, $type, $opts) = @_;
196              
197             # Set undef in "texts" argument position because we are going to create
198             # our own text from the $texts.
199 46         176 my $info = B::DeparseTree::Node->new($op, $self, $texts, undef,
200             $type, $opts);
201 46         109 $info->{sep} = $sep;
202 46         72 my $text = '';
203 46         84 foreach my $item (@$texts) {
204 181 100 100     428 $text .= $sep if $text and $sep;
205 181 50       295 if(ref($item) eq 'ARRAY'){
    50          
206 0         0 $text .= $item->[0];
207 181         862 } elsif (eval{$item->isa("B::DeparseTree::Node")}) {
208 0         0 $text .= $item->{text};
209             } else {
210 181         344 $text .= $item;
211             }
212             }
213              
214 46         125 $info->{text} = $text;
215 46 100       128 if ($opts->{maybe_parens}) {
216 4         6 my ($obj, $context, $precedence) = @{$opts->{maybe_parens}};
  4         9  
217 4         9 my $parens = B::DeparseTree::Node::parens_test($obj, $context, $precedence);
218             $self->{maybe_parens} = {
219             context => $context,
220             precedence => $precedence,
221 4 50       19 force => $obj->{'parens'},
222             parens => $parens ? 'true' : ''
223             };
224 4 50 33     14 $info->{text} = "($info->{text})" if exists $info->{text} and $parens;
225             }
226              
227 46         188 return $info
228             }
229              
230             # Create an info structure a template pattern
231             sub info_from_template($$$$$) {
232 12049     12049 0 25182 my ($self, $type, $op, $fmt, $indexes, $args, $opts) = @_;
233 12049 100       23036 $opts = {} unless defined($opts);
234 12049         19647 my @args = @$args;
235 12049         29643 my $info = B::DeparseTree::Node->new($op, $self, $args, undef, $type, $opts);
236              
237 12049 100       24651 $indexes = [0..$#args] unless defined $indexes;
238 12049         18670 $info->{'indexes'} = $indexes;
239 12049         21667 my $text = $self->template_engine($fmt, $indexes, $args);
240              
241 12049         22952 $info->{'fmt'} = $fmt;
242 12049         18686 $info->{'text'} = $self->template_engine($fmt, $indexes, $args);
243              
244 12049 100       21508 if (! defined $op) {
245 2654         4102 $info->{addr} = ++$self->{'last_fake_addr'};
246 2654         7566 $self->{optree}{$info->{addr}} = $info;
247             }
248              
249 12049 50       18713 if ($opts->{'relink_children'}) {
250             # FIXME we should specify which children to relink
251 0         0 for (my $i=0; $i < scalar @$args; $i++) {
252 0 0       0 if ($args[$i]->isa("B::DeparseTree::Node")) {
253 0         0 $args[$i]{parent} = $info->{addr};
254             }
255             }
256             }
257              
258             # Link the parent of Deparse::Tree::Nodes to this node.
259 12049 50       17725 if ($opts->{'synthesized_nodes'}) {
260 0         0 foreach my $node (@{$opts->{'synthesized_nodes'}}) {
  0         0  
261 0         0 $node->{parent} = $info->{addr};
262             }
263             }
264              
265             # Need to handle maybe_parens since B::DeparseNode couldn't do that
266             # as it was passed a ref ARRAY rather than a string.
267 12049 100       19307 if ($opts->{maybe_parens}) {
268 1581         2107 my ($obj, $context, $precedence) = @{$opts->{maybe_parens}};
  1581         3420  
269 1581         3721 my $parens = B::DeparseTree::Node::parens_test($obj,
270             $context, $precedence);
271             $info->{maybe_parens} = {
272             context => $context,
273             precedence => $precedence,
274 1581 100       8371 force => $obj->{'parens'},
275             parens => $parens ? 'true' : ''
276             };
277 1581 100 66     5598 $info->{text} = "($info->{text})" if exists $info->{text} and $parens;
278             }
279              
280 12049         42681 return $info;
281             }
282              
283             # Create an info structure from a single string
284             sub info_from_string($$$$$)
285             {
286 11297     11297 0 21154 my ($self, $type, $op, $str, $opts) = @_;
287 11297   100     30802 $opts ||= {};
288 11297         26193 return B::DeparseTree::Node->new($op, $self, $str, undef,
289             $type, $opts);
290             }
291              
292             # OBSOLETE: Create an info structure from a single string
293             # FIXME: remove this
294             sub info_from_text($$$$$)
295             {
296 21     21 0 55 my ($op, $self, $text, $type, $opts) = @_;
297             # Use this to smoke outt calls
298             # use Enbugger 'trepan'; Enbugger->stop;
299 21         50 return $self->info_from_string($type, $op, $text, $opts)
300             }
301              
302             # List of suffix characters that are handled by "expand_simple_spec()".
303 3     3   20 use constant SIMPLE_SPEC => '%+-|';
  3         5  
  3         2372  
304              
305             # Extract the string at $args[$index] and if
306             # we are looking for that position include where we are in
307             # that position
308             sub get_info_and_str($$$)
309             {
310 104836     104836 0 140707 my ($self, $index, $args) = @_;
311 104836         126353 my $info = $args->[$index];
312 104836         134120 my $str = $self->info2str($info);
313 104836         195071 return ($info, $str);
314             }
315              
316             sub template_engine($$$$)
317             {
318 76227     76227 0 119337 my ($self, $fmt, $indexes, $args, $find_addr) = @_;
319              
320             # use Data::Dumper;
321             # print "-----\n";
322             # p $args;
323             # print "'======\n";
324             # print $fmt, "\n"
325             # print $args, "\n";
326              
327 76227         82688 my $i = 0;
328 76227 50       104819 $find_addr = -2 unless $find_addr;
329              
330 76227         82976 my $start_fmt = $fmt; # used in error messages
331 76227         103169 my @args = @$args;
332              
333 76227         84472 my $result = '';
334 76227         80113 my $find_pos = undef;
335 76227         139279 while ((my $k=index($fmt, '%')) >= 0) {
336 73449         108999 $result .= substr($fmt, 0, $k);
337 73449         89623 my $spec = substr($fmt, $k, 2);
338 73449         90923 $fmt = substr($fmt, $k+2);
339              
340 73449 100       157233 if (index(SIMPLE_SPEC, substr($spec, 1, 1)) >= 0) {
    100          
    100          
    50          
    100          
    50          
    0          
341 8289         12718 $result .= $self->expand_simple_spec($spec);
342             } elsif ($spec eq "%c") {
343             # Insert child entry
344              
345             # FIXME: turn this into a subroutine.
346 39409 50       43527 if ($i >= scalar @{$indexes}) {
  39409         65210  
347 0         0 Carp::confess("Need another entry in args_spec for %c in fmt: $start_fmt");
348             }
349 39409         57861 my $index = $indexes->[$i++];
350 39409 50       57759 if ($index >= scalar @args) {
351 0         0 Carp::confess("$index in $start_fmt for %c is too large; should be less " .
352             "than scalar(@args)");
353             }
354 39409         42008 my $str;
355 39409         59871 my ($info, $str) = $self->get_info_and_str($index, $args);
356 39409 50 66     108894 if (ref($info) && $info->{'addr'} == $find_addr) {
357 0         0 my $len = length($result);
358             $len++ if (exists $info->{maybe_parens}
359 0 0 0     0 && $info->{maybe_parens}{parens});
360 0         0 $find_pos = [$len, length($str)];
361             }
362 39409         89573 $result .= $str;
363             } elsif ($spec eq "%C") {
364             # Insert separator between child entry lists
365 6666         8037 my ($low, $high, $sub_spec) = @{$indexes->[$i++]};
  6666         13040  
366 6666         11313 my $sep = $self->expand_simple_spec($sub_spec);
367 6666         8753 my $list = '';
368 6666         11109 for (my $j=$low; $j<=$high; $j++) {
369 25101 100       39748 $result .= $sep if $j > $low;
370              
371             # FIXME: Remove duplicate code
372 25101         38379 my ($info, $str) = $self->get_info_and_str($j, $args);
373 25101 50 33     67418 if (ref($info) && $info->{'addr'} == $find_addr) {
374 0         0 my $len = length($result);
375             $len++ if (exists $info->{maybe_parens}
376 0 0 0     0 && $info->{maybe_parens}{parens});
377 0         0 $find_pos = [$len, length($str)];
378             }
379 25101         51831 $result .= $str;
380             }
381             } elsif ($spec eq "%f") {
382             # Run maybe_parens_func
383 0         0 my $fn_name = shift @args;
384 0         0 my ($cx, $prec) = @{$indexes->[$i++]};
  0         0  
385 0         0 my $params = $self->template_engine("%C", [[0, $#args], ', ']);
386 0         0 $result .= B::Deparse::maybe_parens_func($self, $fn_name, $params, $cx, $prec);
387             } elsif ($spec eq "%F") {
388             # Run a transformation function
389 901 50       1618 if ($i >= scalar@$indexes) {
390 0         0 Carp::confess("Need another entry in args_spec for %%F fmt: $start_fmt");
391             }
392 901         1030 my ($arg_index, $transform_fn) = @{$indexes->[$i++]};
  901         1612  
393 901 50       1656 if ($arg_index >= scalar @args) {
394 0         0 Carp::confess("argument index $arg_index in $start_fmt for %%F is too large; should be less than @$args");
395             }
396 901 50       1915 if (ref($transform_fn ne 'CODE')) {
397 0         0 Carp::confess("transformation function $transform_fn is not CODE");
398             }
399 901         1378 my ($arg) = $args[$arg_index];
400 901         1786 $result .= $transform_fn->($arg);
401              
402             } elsif ($spec eq "%;") {
403             # Insert semicolons and indented newlines between statements.
404             # Don't insert them around empty strings - some OPs
405             # don't have an text associated with them.
406             # Finally, replace semicolon a the end of statement that
407             # end in "}" with a \n and proper indent.
408 18184         28981 my $sep = $self->expand_simple_spec(";\n%|");
409 18184         22709 my $start_size = length($result);
410 18184         35779 for (my $j=0; $j< @args; $j++) {
411 40326         50810 my $old_result = $result;
412 40326 100 100     89762 if ($j > 0 && length($result) > $start_size) {
413             # Remove any prior ;\n
414 26788 100       47006 $result = substr($result, 0, -1) if substr($result, -1) eq "\n";
415 26788 100       41803 $result = substr($result, 0, -1) if substr($result, -1) eq ";";
416             ## The below needs to be done based on whether the previous construct is a compound statement or not.
417             ## That could be added in a trailing format specifier for it.
418             ## "sub {...}" and "$h = {..}" need a semicolon while "if () {...}" doesn't.
419             # if (substr($result, -1) eq "}" & $j < $#args) {
420             # # Omit ; from sep. FIXME: do this based on an option?
421             # $result .= substr($sep, 1);
422             # } else {
423             # $result .= $sep;
424             # }
425 26788         35538 $result .= $sep;
426             }
427              
428             # FIXME: Remove duplicate code
429 40326         62108 my ($info, $str) = $self->get_info_and_str($j, $args);
430 40326 50 66     103385 if (ref($info) && $info->{'addr'} == $find_addr) {
431 0         0 my $len = length($result);
432 0 0 0     0 $len++ if exists $info->{maybe_parens} and $info->{maybe_parens}{parens};
433 0         0 $find_pos = [length($result), length($str)];
434             }
435 40326 100       55372 if (!$str) {
436 3352         7844 $result = $old_result;
437             } else {
438 36974         92281 $result .= $str
439             }
440             }
441             # # FIXME: Add the final ';' based on an option?
442             # if ($result and not
443             # (substr($result, -1) eq ';' or
444             # (substr($result, -1) eq ';\n'))) {
445             # $result .= ';' if $result and substr($result, -1) ne ';';
446             # }
447              
448             } elsif ($spec eq "\cS") {
449             # FIXME: not handled yet
450             ;
451             } else {
452             # We have % with a non-special symbol. Just preserve those.
453 0         0 $result .= $spec;
454             }
455             }
456 76227 100       117249 $result .= $fmt if $fmt;
457 76227 50       107840 if ($find_addr != -2) {
458             # want result and position
459 0         0 return $result, $find_pos;
460             }
461             # want just result
462 76227         161801 return $result;
463              
464             }
465              
466             sub template2str($$) {
467 52129     52129 0 67868 my ($self, $info) = @_;
468             return $self->template_engine($info->{fmt},
469             $info->{indexes},
470 52129         87457 $info->{texts});
471             }
472              
473             1;