File Coverage

lib/B/DeparseTree/SyntaxTree.pm
Criterion Covered Total %
statement 161 219 73.5
branch 86 138 62.3
condition 22 50 44.0
subroutine 15 16 93.7
pod 0 14 0.0
total 284 437 64.9


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 8     8   2887 use B::DeparseTree::TreeNode;
  8         27  
  8         13907  
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 0     0 0 0 my ($self, $sep, $items) = @_;
43             # FIXME: loop over $item, testing type.
44 0 0       0 Carp::confess("should be a reference to a array: is $items") unless
45             ref $items eq 'ARRAY';
46 0         0 my @result = ();
47 0         0 foreach my $item (@$items) {
48 0         0 my $add;
49 0 0       0 if (ref $item) {
50 0 0 0     0 if (ref $item eq 'ARRAY' and scalar(@$item) == 2) {
    0          
51 0         0 $add = [$item->[0], $item->[1]];
52 0         0 } elsif (eval{$item->isa("B::DeparseTree::TreeNode")}) {
53 0         0 $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 0 0 0     0 push @result, $sep if @result && $sep;
62 0         0 push @result, $add;
63             }
64 0         0 return @result;
65             }
66              
67             sub combine2str($$$)
68             {
69 168     168 0 285 my ($self, $sep, $items) = @_;
70 168         214 my $result = '';
71 168         287 foreach my $item (@$items) {
72 728 100       1070 $result .= $sep if $result;
73 728 100       920 if (ref $item) {
74 2 50 33     6 if (ref $item eq 'ARRAY' and scalar(@$item) == 2) {
    50          
75             # First item is text and second item is op address.
76 0         0 $result .= $self->info2str($item->[0]);
77 2         7 } elsif (eval{$item->isa("B::DeparseTree::TreeNode")}) {
78 2 50       5 if (exists $item->{fmt}) {
79 2         5 $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 726 50       1076 if (index($item, '@B::DeparseTree::TreeNode') > 0) {
89 0         0 Carp::confess("\@B::DeparseTree::TreeNode as an item is probably wrong");
90             }
91 726         1032 $result .= $item;
92             }
93             }
94 168         370 return $result;
95             }
96              
97             sub expand_simple_spec($$)
98             {
99 38331     38331 0 55614 my ($self, $fmt) = @_;
100 38331         43016 my $result = '';
101 38331         67106 while ((my $k=index($fmt, '%')) >= 0) {
102 26583         35044 $result .= substr($fmt, 0, $k);
103 26583         34500 my $spec = substr($fmt, $k, 2);
104 26583         32801 $fmt = substr($fmt, $k+2);
105              
106 26583 50       55666 if ($spec eq '%%') {
    100          
    100          
    50          
107 0         0 $result .= '%';
108             } elsif ($spec eq '%+') {
109 2785         5244 $result .= $self->indent_more();
110             } elsif ($spec eq '%-') {
111 2785         5027 $result .= $self->indent_less();
112             } elsif ($spec eq '%|') {
113 21013         31420 $result .= $self->indent_value();
114             } else {
115 0         0 Carp::confess("Unknown spec $spec")
116             }
117             }
118 38331 100       60768 $result .= $fmt if $fmt;
119 38331         67307 return $result;
120             }
121              
122             sub indent_less($$) {
123 2785     2785 0 4608 my ($self, $check_level) = @_;
124 2785 50       5440 $check_level = 0 if !defined $check_level;
125              
126 2785         4404 $self->{level} -= $self->{'indent_size'};
127 2785         3716 my $level = $self->{level};
128 2785 50       4755 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 2785         4892 return $self->indent_value();
134             }
135              
136             sub indent_more($) {
137 2785     2785 0 4085 my ($self) = @_;
138 2785         4400 $self->{level} += $self->{'indent_size'};
139 2785         4407 return $self->indent_value();
140             }
141              
142             sub indent_value($) {
143 26583     26583 0 33562 my ($self) = @_;
144 26583         33154 my $level = $self->{level};
145 26583 50       38221 if ($self->{'use_tabs'}) {
146 0         0 return "\t" x ($level / 8) . " " x ($level % 8);
147             } else {
148 26583         87436 return " " x $level;
149             }
150             }
151              
152             sub info2str($$)
153             {
154 122031     122031 0 151527 my ($self, $item) = @_;
155 122031         136886 my $result = '';
156 122031 100       180980 if (ref $item) {
157 114088 50 33     222137 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 114088         266725 } elsif (eval{$item->isa("B::DeparseTree::TreeNode")}) {
162 114088 100 66     234704 if (exists $item->{fmt}) {
    100          
163 52066         85421 $result .= $self->template2str($item);
164 52066 100       99313 if ($item->{maybe_parens}) {
165 7243         10064 my $mp = $item->{maybe_parens};
166 7243 100 66     21498 if ($mp->{force} || $mp->{parens}) {
167 306         733 $result = "($result)";
168             }
169             }
170             } elsif (!exists $item->{texts} && exists $item->{text}) {
171             # Is a constant string
172 61856         89452 $result .= $item->{text};
173             } else {
174             $result = $self->combine2str($item->{sep},
175 166         361 $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 7943 50       14984 if (index($item, '@B::DeparseTree::TreeNode') > 0) {
184 0         0 Carp::confess("\@B::DeparseTree::TreeNode as an item is probably wrong");
185             }
186 7943         10488 $result = $item;
187             }
188 122031         211144 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 29     29 0 87 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 29         110 my $info = B::DeparseTree::TreeNode->new($op, $self, $texts, undef,
200             $type, $opts);
201 29         75 $info->{sep} = $sep;
202 29         47 my $text = '';
203 29         57 foreach my $item (@$texts) {
204 116 100 100     309 $text .= $sep if $text and $sep;
205 116 50       217 if(ref($item) eq 'ARRAY'){
    50          
206 0         0 $text .= $item->[0];
207 116         606 } elsif (eval{$item->isa("B::DeparseTree::TreeNode")}) {
208 0         0 $text .= $item->{text};
209             } else {
210 116         255 $text .= $item;
211             }
212             }
213              
214 29         70 $info->{text} = $text;
215 29 100       74 if ($opts->{maybe_parens}) {
216 4         4 my ($obj, $context, $precedence) = @{$opts->{maybe_parens}};
  4         36  
217 4         12 my $parens = B::DeparseTree::TreeNode::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     23 $info->{text} = "($info->{text})" if exists $info->{text} and $parens;
225             }
226              
227 29         126 return $info
228             }
229              
230             # Create an info structure a template pattern
231             sub info_from_template($$$$$) {
232 12206     12206 0 26969 my ($self, $type, $op, $fmt, $indexes, $args, $opts) = @_;
233 12206 100       23504 $opts = {} unless defined($opts);
234             # if (ref($args) ne "ARRAY") {
235             # use Enbugger "trepan"; Enbugger->stop;
236             # }
237 12206         20289 my @args = @$args;
238 12206         29965 my $info = B::DeparseTree::TreeNode->new($op, $self, $args, undef, $type, $opts);
239              
240 12206 100       30048 $indexes = [0..$#args] unless defined $indexes;
241 12206         19824 $info->{'indexes'} = $indexes;
242 12206         24065 my $text = $self->template_engine($fmt, $indexes, $args);
243              
244 12206         26289 $info->{'fmt'} = $fmt;
245 12206         21412 $info->{'text'} = $self->template_engine($fmt, $indexes, $args);
246              
247 12206 100       24179 if (! defined $op) {
248 1341         2742 $info->{addr} = ++$self->{'last_fake_addr'};
249 1341         4266 $self->{optree}{$info->{addr}} = $info;
250             }
251              
252 12206 50       20887 if ($opts->{'relink_children'}) {
253             # FIXME we should specify which children to relink
254 0         0 for (my $i=0; $i < scalar @$args; $i++) {
255 0 0       0 if ($args[$i]->isa("B::DeparseTree::TreeNode")) {
256 0         0 $args[$i]{parent} = $info->{addr};
257             }
258             }
259             }
260              
261             # Link the parent of Deparse::Tree::TreeNodes to this node.
262 12206 50       19314 if ($opts->{'synthesized_nodes'}) {
263 0         0 foreach my $node (@{$opts->{'synthesized_nodes'}}) {
  0         0  
264 0         0 $node->{parent} = $info->{addr};
265             }
266             }
267              
268             # Need to handle maybe_parens since B::DeparseNode couldn't do that
269             # as it was passed a ref ARRAY rather than a string.
270 12206 100       18813 if ($opts->{maybe_parens}) {
271 1560         2251 my ($obj, $context, $precedence) = @{$opts->{maybe_parens}};
  1560         3366  
272 1560         4511 my $parens = B::DeparseTree::TreeNode::parens_test($obj,
273             $context, $precedence);
274             $info->{maybe_parens} = {
275             context => $context,
276             precedence => $precedence,
277 1560 100       9132 force => $obj->{'parens'},
278             parens => $parens ? 'true' : ''
279             };
280 1560 100 66     6098 $info->{text} = "($info->{text})" if exists $info->{text} and $parens;
281             }
282              
283 12206         42921 return $info;
284             }
285              
286             # Create an info structure from a single string
287             sub info_from_string($$$$$)
288             {
289 13779     13779 0 28034 my ($self, $type, $op, $str, $opts) = @_;
290 13779   100     40858 $opts ||= {};
291 13779         36674 return B::DeparseTree::TreeNode->new($op, $self, $str, undef,
292             $type, $opts);
293             }
294              
295             # OBSOLETE: Create an info structure from a single string
296             # FIXME: remove this
297             sub info_from_text($$$$$)
298             {
299 18     18 0 41 my ($op, $self, $text, $type, $opts) = @_;
300             # Use this to smoke outt calls
301             # use Enbugger 'trepan'; Enbugger->stop;
302 18         40 return $self->info_from_string($type, $op, $text, $opts)
303             }
304              
305             # List of suffix characters that are handled by "expand_simple_spec()".
306 8     8   74 use constant SIMPLE_SPEC => '%+-|';
  8         18  
  8         7931  
307              
308             # Extract the string at $args[$index] and if
309             # we are looking for that position include where we are in
310             # that position
311             sub get_info_and_str($$$)
312             {
313 121023     121023 0 167641 my ($self, $index, $args) = @_;
314 121023         150034 my $info = $args->[$index];
315 121023         166482 my $str = $self->info2str($info);
316 121023         230847 return ($info, $str);
317             }
318              
319             sub template_engine($$$$)
320             {
321 76480     76480 0 123561 my ($self, $fmt, $indexes, $args, $find_addr) = @_;
322              
323             # use Data::Dumper;
324             # print "-----\n";
325             # p $args;
326             # print "'======\n";
327             # print $fmt, "\n"
328             # print $args, "\n";
329              
330 76480         86139 my $i = 0;
331 76480 50       109660 $find_addr = -2 unless $find_addr;
332              
333 76480         86724 my $start_fmt = $fmt; # used in error messages
334 76480         109901 my @args = @$args;
335              
336 76480         87819 my $result = '';
337 76480         82378 my $find_pos = undef;
338 76480         144543 while ((my $k=index($fmt, '%')) >= 0) {
339 88927         138660 $result .= substr($fmt, 0, $k);
340 88927         112619 my $spec = substr($fmt, $k, 2);
341 88927         117302 $fmt = substr($fmt, $k+2);
342              
343 88927 100       196249 if (index(SIMPLE_SPEC, substr($spec, 1, 1)) >= 0) {
    100          
    100          
    50          
    100          
    50          
    0          
344 8320         13955 $result .= $self->expand_simple_spec($spec);
345             } elsif ($spec eq "%c") {
346             # Insert child entry
347              
348             # FIXME: turn this into a subroutine.
349 49716 50       55859 if ($i >= scalar @{$indexes}) {
  49716         80757  
350 0         0 Carp::confess("Need another entry in args_spec for %c in fmt: $start_fmt");
351             }
352 49716         69449 my $index = $indexes->[$i++];
353 49716 50       76300 if ($index >= scalar @args) {
354 0         0 Carp::confess("$index in $start_fmt for %c is too large; should be less " .
355             "than scalar(@args)");
356             }
357 49716         55144 my $str;
358 49716         76888 my ($info, $str) = $self->get_info_and_str($index, $args);
359 49716 50 66     145393 if (ref($info) && $info->{'addr'} == $find_addr) {
360 0         0 my $len = length($result);
361             $len++ if (exists $info->{maybe_parens}
362 0 0 0     0 && $info->{maybe_parens}{parens});
363 0         0 $find_pos = [$len, length($str)];
364             }
365 49716         115665 $result .= $str;
366             } elsif ($spec eq "%C") {
367             # Insert separator between child entry lists
368 11748         13909 my ($low, $high, $sub_spec) = @{$indexes->[$i++]};
  11748         23516  
369 11748         22576 my $sep = $self->expand_simple_spec($sub_spec);
370 11748         16223 my $list = '';
371 11748         22502 for (my $j=$low; $j<=$high; $j++) {
372 31243 100       52038 $result .= $sep if $j > $low;
373              
374             # FIXME: Remove duplicate code
375 31243         50960 my ($info, $str) = $self->get_info_and_str($j, $args);
376 31243 50 33     89346 if (ref($info) && $info->{'addr'} == $find_addr) {
377 0         0 my $len = length($result);
378             $len++ if (exists $info->{maybe_parens}
379 0 0 0     0 && $info->{maybe_parens}{parens});
380 0         0 $find_pos = [$len, length($str)];
381             }
382 31243         68773 $result .= $str;
383             }
384             } elsif ($spec eq "%f") {
385             # Run maybe_parens_func
386 0         0 my $fn_name = shift @args;
387 0         0 my ($cx, $prec) = @{$indexes->[$i++]};
  0         0  
388 0         0 my $params = $self->template_engine("%C", [[0, $#args], ', ']);
389 0         0 $result .= B::Deparse::maybe_parens_func($self, $fn_name, $params, $cx, $prec);
390             } elsif ($spec eq "%F") {
391             # Run a transformation function
392 880 50       1853 if ($i >= scalar@$indexes) {
393 0         0 Carp::confess("Need another entry in args_spec for %%F fmt: $start_fmt");
394             }
395 880         1171 my ($arg_index, $transform_fn) = @{$indexes->[$i++]};
  880         1917  
396 880 50       2176 if ($arg_index >= scalar @args) {
397 0         0 Carp::confess("argument index $arg_index in $start_fmt for %%F is too large; should be less than @$args");
398             }
399 880 50       2293 if (ref($transform_fn ne 'CODE')) {
400 0         0 Carp::confess("transformation function $transform_fn is not CODE");
401             }
402 880         1581 my ($arg) = $args[$arg_index];
403 880         2136 $result .= $transform_fn->($arg);
404              
405             } elsif ($spec eq "%;") {
406             # Insert semicolons and indented newlines between statements.
407             # Don't insert them around empty strings - some OPs
408             # don't have an text associated with them.
409             # Finally, replace semicolon a the end of statement that
410             # end in "}" with a \n and proper indent.
411 18263         29436 my $sep = $self->expand_simple_spec(";\n%|");
412 18263         23238 my $start_size = length($result);
413 18263         36014 for (my $j=0; $j< @args; $j++) {
414 40064         49713 my $old_result = $result;
415 40064 100 100     94063 if ($j > 0 && length($result) > $start_size) {
416             # Remove any prior ;\n
417 26538 100       46528 $result = substr($result, 0, -1) if substr($result, -1) eq "\n";
418 26538 100       41690 $result = substr($result, 0, -1) if substr($result, -1) eq ";";
419             ## The below needs to be done based on whether the previous construct is a compound statement or not.
420             ## That could be added in a trailing format specifier for it.
421             ## "sub {...}" and "$h = {..}" need a semicolon while "if () {...}" doesn't.
422             # if (substr($result, -1) eq "}" & $j < $#args) {
423             # # Omit ; from sep. FIXME: do this based on an option?
424             # $result .= substr($sep, 1);
425             # } else {
426             # $result .= $sep;
427             # }
428 26538         35691 $result .= $sep;
429             }
430              
431             # FIXME: Remove duplicate code
432 40064         64115 my ($info, $str) = $self->get_info_and_str($j, $args);
433 40064 50 66     107603 if (ref($info) && $info->{'addr'} == $find_addr) {
434 0         0 my $len = length($result);
435 0 0 0     0 $len++ if exists $info->{maybe_parens} and $info->{maybe_parens}{parens};
436 0         0 $find_pos = [length($result), length($str)];
437             }
438 40064 100       56926 if (!$str) {
439 3507         8270 $result = $old_result;
440             } else {
441 36557         98972 $result .= $str
442             }
443             }
444             # # FIXME: Add the final ';' based on an option?
445             # if ($result and not
446             # (substr($result, -1) eq ';' or
447             # (substr($result, -1) eq ';\n'))) {
448             # $result .= ';' if $result and substr($result, -1) ne ';';
449             # }
450              
451             } elsif ($spec eq "\cS") {
452             # FIXME: not handled yet
453             ;
454             } else {
455             # We have % with a non-special symbol. Just preserve those.
456 0         0 $result .= $spec;
457             }
458             }
459 76480 100       125946 $result .= $fmt if $fmt;
460 76480 50       109027 if ($find_addr != -2) {
461             # want result and position
462 0         0 return $result, $find_pos;
463             }
464             # want just result
465 76480         167455 return $result;
466              
467             }
468              
469             sub template2str($$) {
470 52068     52068 0 68968 my ($self, $info) = @_;
471             return $self->template_engine($info->{fmt},
472             $info->{indexes},
473 52068         89029 $info->{texts});
474             }
475              
476             1;