File Coverage

lib/Devel/Graph.pm
Criterion Covered Total %
statement 232 270 85.9
branch 95 122 77.8
condition 20 28 71.4
subroutine 25 31 80.6
pod 9 9 100.0
total 381 460 82.8


line stmt bran cond sub pod time code
1             #############################################################################
2             # Generate flowcharts from Perl code.
3             #
4             #############################################################################
5              
6             package Devel::Graph;
7              
8 5     5   764337 use Graph::Easy;
  5         1375454  
  5         262  
9 5     5   68 use Graph::Easy::Base;
  5         8  
  5         89  
10 5     5   8666 use Graph::Flowchart;
  5         29075  
  5         233  
11 5         640 use Graph::Flowchart::Node qw/
12             N_IF N_THEN N_ELSE N_JOINT N_BLOCK N_BODY
13             N_SUB N_RETURN
14             N_BREAK N_LAST N_GOTO N_CONTINUE N_NEXT
15             N_FOR N_WHILE N_UNTIL
16 5     5   51 /;
  5         12  
17              
18             $VERSION = '0.12';
19              
20             @ISA = qw/Graph::Easy::Base/;
21             @EXPORT_OK = qw/graph/;
22              
23 5     5   30 use strict;
  5         10  
  5         185  
24 5     5   5371 use PPI;
  5         830698  
  5         16226  
25              
26             #############################################################################
27             #############################################################################
28              
29             sub _init
30             {
31 21     21   465366 my ($self, $args) = @_;
32              
33 21         168 $self->{options} = {};
34              
35 21         89 $self->{opt}->{strip_pod} = 1;
36 21 0       444 $self->{opt}->{strip_pod} = ($args->{strip_pod} ? 1 : 0)
    50          
37             if defined $args->{strip_pod};
38              
39 21         74 $self->{fatal_errors} = $args->{fatal_errors};
40 21 50       101 $self->{fatal_errors} = 1 unless defined $self->{fatal_errors};
41              
42 21   50     172 $self->{debug} = $args->{debug} || 0;
43              
44 21         126 $self->reset();
45              
46 21         76 $self;
47             }
48              
49             sub option
50             {
51 1     1 1 500 my $self = shift;
52              
53 1         6 $self->{opt}->{$_[0]};
54             }
55              
56             sub debug
57             {
58 0     0 1 0 my $self = shift;
59              
60 0 0       0 $self->{debug} = $_[0] if @_;
61 0         0 $self->{debug};
62             }
63              
64             sub graph
65             {
66             # decompose code and return as Graph::Easy object
67              
68             # allow the following styles:
69             # Devel::Graph->graph($code); @_ == 2 $class
70             # Devel::Graph::graph($code); @_ == 1
71             # $grapher->graph($code); @_ == 2 ref($self) == $class
72              
73 18     18 1 8814 my $self = 'Devel::Graph';
74 18 50       90 $self = shift if @_ == 2;
75 18         41 my $code = shift;
76              
77 18 100       68 if (! ref($self) )
78             {
79 1         6 $self = $self->new();
80             }
81 18         64 $self->reset();
82 18         73 $self->decompose($code);
83 15         3223 $self->{flow}->finish();
84              
85 15         7578 $self->{flow}->as_graph(); # return the Graph::Easy object
86             }
87              
88             sub as_graph
89             {
90             # return the internal Graph::Easy object
91 0     0 1 0 my $self = shift;
92              
93 0         0 $self->{flow}->as_graph();
94             }
95              
96             sub as_ascii
97             {
98             # return the flowchart as ASCII
99 1     1 1 940 my $self = shift;
100              
101 1         6 $self->{flow}->as_graph()->as_ascii();
102             }
103              
104             sub as_flowchart
105             {
106             # return the internal Graph::Flowchart object
107 0     0 1 0 my $self = shift;
108              
109 0         0 $self->{flow};
110             }
111              
112             sub decompose
113             {
114 26     26 1 6075 my ($self, $code) = @_;
115              
116 26 50 66     234 $self->error("Expected SCALAR ref, but got " . ref($code))
117             if ref($code) && ref($code) ne 'SCALAR';
118              
119 26 50 66     136 $self->error("Got filename '$code', but can't read it: $!")
120             if !ref($code) && !-f $code;
121              
122 24         272 my $doc = PPI::Document->new($code);
123              
124 24 50       89922 $self->error("Couldn't create PPI::Document from $code")
125             unless ref($doc);
126              
127 24 50       221 $doc->prune('PPI::Token::Pod') if $self->{opt}->{strip_pod};
128              
129 24         41641 $self->_parse($doc);
130              
131 21         942 $self;
132             }
133              
134             sub finish
135             {
136 0     0 1 0 my $self = shift;
137              
138 0         0 $self->{flow}->finish();
139             }
140              
141             sub reset
142             {
143             # reset the internal structure
144 55     55 1 118 my $self = shift;
145              
146 55         278 Graph::Easy::Base->_reset_id();
147              
148 55         169 $self->{cur_group} = undef;
149 55         139 $self->{stack} = [];
150 55         361 $self->{flow} = Graph::Flowchart->new();
151 55         54378 $self->{flow}->{graph}->seed(0);
152              
153 55         2096 $self;
154             }
155              
156             #############################################################################
157             #############################################################################
158             # _parse helper routines
159              
160             sub _find_first
161             {
162             # return the first child of $element matching any of the given types
163 16     16   49 my $self = shift;
164 16         31 my $element = shift;
165              
166 16         26 for my $child (@{$element->{children}})
  16         45  
167             {
168 67         110 for my $type (@_)
169             {
170 67 100       440 return $child if $child->isa($type);
171             }
172             }
173              
174 3         25 undef;
175             }
176              
177             sub _find_second
178             {
179             # return the first child of $element matching any of the given types
180 1     1   3 my $self = shift;
181 1         3 my $element = shift;
182              
183 1 50       5 $self->error("Got non-object as element: $element") unless ref $element;
184              
185 1         3 my @blocks;
186 1         2 for my $child (@{$element->{children}})
  1         5  
187             {
188 3         8 for my $type (@_)
189             {
190 3 100       16 push @blocks, $child if $child->isa($type);
191 3 100       79 return $blocks[-1] if scalar @blocks == 2;
192             }
193             }
194              
195 0         0 undef;
196             }
197              
198             sub _find_on_stack
199             {
200 0     0   0 my ($self, $type) = @_;
201              
202 0         0 my $stack = $self->{stack};
203              
204 0         0 for my $e (@$stack)
205             {
206 0 0       0 return $e if $e->{_type} == $type;
207             }
208              
209 0         0 undef;
210             }
211              
212             #############################################################################
213             #############################################################################
214              
215             sub _parse_compound
216             {
217 16     16   38 my ($self, $element, $type) = @_;
218              
219             # work around bug in PPI
220 16 50       101 $type = $element->type() unless defined $type;
221              
222 16 50       789 $self->error("Cannot determine type of compound element $element")
223             unless defined $type;
224              
225             # closures (bug #29346)
226 16 100       54 return $self->_parse($element->children)
227             if $type eq 'continue';
228              
229 15 100 66     110 return $self->_parse_loop($element)
230             if $type eq 'for' || $type eq 'foreach';
231            
232 14 100       69 $self->error("Cannot find condition: possible syntax error in $element")
233             unless defined $self->_find_first($element,'PPI::Structure::Condition');
234              
235             # ignoring whitespace and comments, find the condition
236 11         17 my @blocks;
237              
238 11         66 my $condition = $element->clone();
239              
240 11         11611 my $in_cond = 0;
241 11         19 my @children = @{$condition->{children}};
  11         46  
242              
243 11         22 for my $child (@children)
244             {
245 62 100       195 next unless $child->significant();
246              
247 37 100       163 if ($child->isa('PPI::Structure::Block'))
248             {
249 13         27 push @blocks, $child;
250 13         17 $in_cond = 2;
251             }
252              
253 37 100       165 $child->remove() unless $in_cond == 1;
254              
255 37 100 66     1081 $in_cond = 1 if $in_cond == 0 && $child->isa('PPI::Token::Word');
256             }
257              
258             ########################################################################
259             ########################################################################
260             # work around bug in PPI for "unless" having type() return 'if'
261 11 100       48 if ($type eq 'if')
262             {
263 7         39 my $c = $element->find_first('PPI::Token::Word');
264 7 100       1489 $type = 'unless' if $c eq 'unless';
265             }
266             ########################################################################
267             ########################################################################
268              
269             ########################################################################
270             ########################################################################
271             # work around bug in PPI for "until" having type() return 'while'
272 11 100       171 if ($type eq 'while')
273             {
274 4         22 my $c = $element->find_first('PPI::Token::Word');
275 4 100       768 $type = 'until' if $c eq 'until';
276             }
277             ########################################################################
278             ########################################################################
279              
280 11         182 $condition = $self->_normalize_condition($condition);
281              
282 11 100       63 return $self->_parse_if($type, $condition, @blocks)
283             if $type =~ /^(if|unless)\z/;
284              
285 4 50       445 return $self->_parse_while($type, $condition, @blocks)
286             if $type =~ /^(until|while)\z/;
287              
288 0         0 $self->error("Unknown conditional type $type");
289             }
290              
291             sub _normalize_condition
292             {
293 11     11   21 my ($self, $condition) = @_;
294              
295 11         38 my $text = $condition->content();
296              
297 11         634 $text =~ s/^\s+//;
298 11         210 $text =~ s/\s+\z//;
299              
300 11         30 $text;
301             }
302              
303             sub _parse_if
304             {
305 9     9   828 my ($self, $type, $condition, $block, $else_block) = @_;
306              
307 9 50       28 $self->error('Undefined block in if expression') unless defined $block;
308              
309 9         26 my $flow = $self->{flow};
310              
311             # cur => if => then => joint
312             # |--------------^
313            
314 9         63 my $if = $flow->add_new_block('if ' . $condition, N_IF());
315              
316 9         2386 my @edges = ('true','false');
317 9 100       36 @edges = ('false','true') if $type eq 'unless';
318              
319             # cur => if => then
320 9         69 my $then = $flow->add_joint();
321 9         830 my $then_edge = $flow->connect($if,$then,$edges[0]);
322              
323 9         1912 $flow->current_block($then);
324              
325             # fill in the "then" block
326 9         112 $self->_parse($block);
327 9         1615 my $last_then = $flow->add_new_joint();
328              
329 9 100       1396 if ($else_block)
330             {
331             # cur => if =====> then => joint
332             # --false-> else => ---^
333              
334 1         6 my $else_joint = $flow->add_joint();
335              
336 1         94 my $else_edge = $flow->connect($if,$else_joint,$edges[1]);
337 1         180 $flow->current_block($else_joint);
338              
339 1         12 $self->_parse($else_block);
340            
341 1         4 my $l_joint = $flow->add_new_joint();
342              
343 1         141 $flow->connect($l_joint,$last_then);
344             }
345             else
346             {
347             # connect the "if" block with the newly added joint
348             # cur => if => then => joint
349             # ----false--------^
350            
351             # add a dummy-joint
352 8         37 $flow->connect($if,$last_then,$edges[1]);
353             }
354 9         1359 $flow->current($last_then);
355             }
356              
357             sub _parse_sub
358             {
359 1     1   3 my ($self, $sub) = @_;
360              
361 1         7 my $name = $self->_find_second($sub, 'PPI::Token::Word');
362            
363 1         4 my $flow = $self->{flow};
364              
365 1         6 $flow->add_group("sub $name:");
366              
367             # remember pos before sub
368 1         127 my $cur = $flow->current();
369              
370             # entry point
371 1         12 my $joint = $flow->add_joint(); $joint->{_label} = $name;
  1         149  
372 1         4 $flow->current($joint);
373              
374             # recurse into our children, but ignore:
375             # whitespace and comments and Null (";")
376 1         10 foreach my $child (@{$sub->{children}})
  1         3  
377             {
378 5 100       23 $self->_parse($child) if $child->significant();
379             }
380              
381             # continue at pos before sub
382 1         5 $flow->current($cur);
383 1         10 $flow->no_group();
384              
385             }
386              
387             sub _parse_while
388             {
389             # add while() or until() loops
390 7     7   45 my ($self, $type, $condition, $body, $continue) = @_;
391              
392             # |----------- false ------------v
393             # while () -- true --> body -> continue *
394             # ^----------------------------|
395              
396 7         19 my $flow = $self->{flow};
397 7 100       19 my $t = N_WHILE; $t = N_UNTIL if $type eq 'until';
  7         27  
398 7         67 my @edges = ('true','false');
399 7 100       30 @edges = ('false','true') if $type eq 'until';
400              
401 7         79 my $while = $flow->add_new_block( "$type $condition", $t);
402 7         1744 my $body_block = $flow->add_joint();
403              
404             # -- true -->
405 7         617 my $true = $flow->connect($while,$body_block, $edges[0]);
406             # make the true edge start at the right side (perpendicular to flow)
407 7 100       1332 $true->set_attribute('start','right') if $type eq 'until';
408              
409 7         515 $body_block->set_attributes( {
410             offset => '-2,0',
411             origin => $while->name(),
412             });
413              
414 7         1745 $flow->current($body_block);
415              
416             # insert the body
417 7         82 $self->_parse($body);
418              
419 7 100       1572 if (defined $continue)
420             {
421             # connect the body to the continue block
422 1         5 my $cont_block = $flow->add_new_joint();
423 1         610 $self->_parse($continue);
424             }
425              
426             # connect body (or continue) back to while
427 7         28 my $back = $flow->connect($flow->current(), $while);
428             # make the back edge end at the right side (perpendicular to flow)
429 7         1266 $back->set_attribute('end','right,1');
430              
431             # connect body to next
432 7         600 my $next = $flow->add_joint();
433              
434 7         648 my $forward = $flow->connect($while, $next, $edges[1]);
435              
436             # make that edge go forwards
437 7         1014 $forward->set_attribute('flow','forward');
438              
439 7         586 $flow->current($next);
440             }
441              
442             sub _parse_loop
443             {
444 1     1   2 my ($self, $element) = @_;
445              
446             # PPI::Statement::Compound
447             # PPI::Token::Word 'for'
448             # PPI::Structure::ForLoop ( ... )
449             # PPI::Statement::Variable
450             # PPI::Token::Word 'my'
451             # PPI::Token::Symbol '$i'
452             # PPI::Token::Operator '='
453             # PPI::Token::Number '0'
454             # PPI::Token::Structure ';'
455             # PPI::Statement
456             # PPI::Token::Symbol '$i'
457             # PPI::Token::Operator '<'
458             # PPI::Token::Number '10'
459             # PPI::Token::Structure ';'
460             # PPI::Statement
461             # PPI::Token::Symbol '$i'
462             # PPI::Token::Operator '++'
463             # PPI::Structure::Block { ... }
464             # PPI::Statement
465             # PPI::Token::Symbol '$b'
466             # PPI::Token::Operator '++'
467             # PPI::Token::Structure ';'
468              
469             # PPI::Statement::Compound
470             # PPI::Token::Word 'for'
471             # PPI::Token::Word 'my'
472             # PPI::Token::Symbol '$i'
473             # PPI::Structure::ForLoop ( ... )
474             # PPI::Statement
475             # PPI::Token::Symbol '@list'
476             # PPI::Structure::Block { ... }
477             # PPI::Statement
478             # PPI::Token::Word 'print'
479             # PPI::Token::Symbol '$foo'
480              
481 1         10 my $loop = $self->_find_first($element, 'PPI::Structure::ForLoop');
482              
483 1         56 my (@bodies, @blocks, @var);
484             # get the stuff inside the ()
485 1         2 foreach my $child (@{$loop->{children}})
  1         3  
486             {
487 5 100       19 push @blocks, $child if $child->isa('PPI::Statement');
488             }
489             # get the body (and continue) block
490 1         3 foreach my $child (@{$element->{children}})
  1         2  
491             {
492 6 100       32 push @bodies, $child if $child->isa('PPI::Structure::Block');
493             }
494             # get the variable in front of the () for foreach loops
495 1         2 foreach my $child (@{$element->{children}})
  1         3  
496             {
497 6 100 66     74 push @var, $child->content() if $child->isa('PPI::Token::Word') || $child->isa('PPI::Token::Symbol');
498             }
499 1         2 shift @var; # remove the "for" so that "for my $i" results in "my $i";
500              
501 1         2 my $flow = $self->{flow};
502 1 50       4 if (@blocks == 1)
503             {
504             # 'for my $var (@list)'
505              
506 0         0 my $v = join(" ", @var);
507 0         0 $blocks[0] = 'for ' . $v . " ($blocks[0])";
508 0         0 push @blocks, '';
509              
510             # |-----last------v
511             # for ---> body *
512             # ^--------|
513            
514             # XXX TODO:
515             # technically, we need to parse $blocks[0]!
516              
517 0         0 my $for_block = $flow->add_new_block($blocks[0], N_FOR());
518 0         0 my $body_block = $flow->add_new_joint();
519              
520             # insert the '*' for "next"
521 0         0 my $next = $flow->add_joint();
522              
523             # insert the body
524 0         0 $self->_parse($bodies[0]);
525              
526             # connect the body back to the for
527 0         0 my $cur = $flow->current();
528 0 0       0 if ($cur->{_type} == N_JOINT)
529             {
530             # XXX TODO: if current is a joint, eliminate it
531             # move all incoming edges to point directly to 'for'
532 0         0 $flow->connect($cur, $for_block, 'next');
533             }
534             else
535             {
536 0         0 $flow->connect($cur, $for_block, 'next');
537             }
538              
539 0         0 my $last = $flow->connect($for_block, $next, 'last');
540 0         0 $last->set_attribute('flow','forward');
541            
542 0         0 $flow->current($next);
543              
544 0         0 return;
545             }
546              
547             # init -> if $while --> body --> cont --> (back to if)
548              
549             # XXX TODO:
550             # technically, we need to parse $blocks[x]!
551              
552 1         5 my $next = $flow->add_joint();
553 1         84 my $for_block = $flow->add_new_block('for: ' . $blocks[0], N_FOR());
554 1         157 my $while_block = $flow->add_new_block('while ' . $blocks[1], N_WHILE());
555            
556 1         156 my $body_block = $flow->add_joint();
557              
558 1         65 $flow->connect($while_block, $body_block, 'true');
559              
560             # insert the body
561 1         178 $flow->current($body_block);
562 1         13 $self->_parse($bodies[0]);
563              
564 1         4 my $cur = $flow->current();
565 1         6 my $cont_block = $flow->add_new_block($blocks[2], N_BLOCK());
566              
567 1         474 my $false = $flow->connect($while_block, $next, 'false');
568 1         120 $false->set_attribute('flow','forward');
569              
570 1         66 $flow->connect($cont_block, $while_block, 'continue');
571              
572 1         114 $flow->current($next);
573             }
574              
575             sub _parse_conditional
576             {
577             # parse a statement with a trailing condition/loop
578 5     5   2402 my ($self, $element) = @_;
579              
580             # PPI::Statement
581             # PPI::Token::Word 'print'
582             # PPI::Token::Symbol '$a'
583             # PPI::Token::Operator '++'
584             # PPI::Token::Word 'if' <-- type
585             # PPI::Structure::Condition ( ... ) <-- condition start
586             # PPI::Statement::Expression
587             # PPI::Token::Symbol '$a'
588             # PPI::Token::Operator '<'
589             # PPI::Token::Number '9'
590            
591             # gather all elements up to the condition
592 5         13 my @blocks;
593             my $condition;
594              
595 5         13 for my $child (@{$element->{children}})
  5         18  
596             {
597 29 100       135 next unless $child->significant(); # ignore whitespace etc
598 19         24 push @blocks, $child;
599 19 100 100     109 $condition = $child->snext_sibling(), last
600             if $child->isa('PPI::Token::Word') && $child =~ /^(if|unless|until|while)\z/;
601             }
602              
603 5         222 my $type = pop @blocks; # if, unless, until, or while
604              
605             # make a copy and delete the condition and the word before it
606             # to get only the block of the condition:
607              
608 5         29 my $block = $element->clone();
609 5         1245 my $c = $block->find_first('PPI::Structure::Condition');
610 5         2122 my $t = $c->sprevious_sibling();
611 5         171 $c->delete();
612 5         629 $t->delete();
613              
614             # delete trailing whitespace in $block (so that "$c = 123 ;" turns in "$c = 123;"
615 5         170 for my $child (reverse @{$block->{children}})
  5         14  
616             {
617             # remove the trailing ";" because otherwise:
618             # "print $a++ if (...)" would turn into "print $a++"
619             # while "print $a++ if (...);" would turn into "print $a++;"
620              
621             # stop at the first significant child other than the ";"
622 18 100 50     696 $child->delete() && next if $child->isa('PPI::Token::Structure') && $child eq ';';
      66        
623 15 100       46 last if $child->significant();
624 10         30 $child->delete();
625             }
626            
627 5 100       14 return $self->_parse_if($type, $condition, $block)
628             if $type =~ /^(if|unless)\z/;
629              
630 3 50       28 return $self->_parse_while($type, $condition, $block)
631             if $type =~ /^(until|while)\z/;
632              
633 0         0 $self->error("Unknown conditional type $type");
634             }
635              
636             my $types = {
637             'return' => N_RETURN(),
638             'last' => N_LAST(),
639             'break' => N_BREAK(),
640             'continue' => N_CONTINUE(),
641             'goto' => N_GOTO(),
642             'next' => N_NEXT(),
643             };
644              
645             sub _parse_break
646             {
647 1     1   3 my ($self, $element) = @_;
648              
649             # find the type of the break statement
650 1         4 my $type = $self->_find_first($element, 'PPI::Token::Word');
651              
652 1         4 my $flow = $self->{flow};
653              
654 1         2 my $target;
655 1 50       5 if ($type ne 'return')
656             {
657 0         0 my $t = $types->{"$type"};
658 0 0       0 $self->error("Unrecognized break type $type") unless defined $t;
659              
660             # ignore first Token::Word
661 0         0 $target = $self->_find_second($element, 'PPI::Token::Word');
662 0         0 $flow->add_jump(
663             $element->content(), # "last FOO;"
664             $t, # N_BREAK etc
665             '',
666             $target->content()); # "FOO"
667             }
668             else
669             {
670 1         23 $flow->add_new_block($element->content(), N_RETURN(), '');
671             }
672             }
673              
674             #############################################################################
675              
676             sub _parse_expression
677             {
678 40     40   73 my ($self, $element) = @_;
679              
680 40         76 my $flow = $self->{flow};
681              
682 40         254 $flow->add_new_block( $element->content(), N_BLOCK());
683             }
684              
685             #############################################################################
686             #############################################################################
687             # main parse routine, recursive
688              
689             sub _error
690             {
691 0     0   0 require Carp;
692              
693 0         0 Carp::confess($_);
694             }
695              
696             sub _parse
697             {
698             # take a PPI::ELement and descend into it recursively
699 5     5   68 no warnings 'recursion';
  5         9  
  5         1410  
700 105     105   175 my ($self, $element) = @_;
701              
702             # print STDERR "parsing ", ref($element)," ($element)\n";
703              
704 105 50       266 $self->error('Encountered an undefined element while parsing')
705             unless defined $element;
706              
707             # handle 'if', 'while', 'for', 'until' as compound statements
708             # Example: until ($a < 9) { $b++; }
709 105 100       719 return $self->_parse_compound($element)
710             if $element->isa('PPI::Statement::Compound');
711              
712             # handle sub
713 89 100       598 return $self->_parse_sub($element)
714             if $element->isa('PPI::Statement::Sub');
715              
716             # handle next, last, return and break
717 88 100       468 return $self->_parse_break($element)
718             if $element->isa('PPI::Statement::Break');
719              
720             # Example: "$a = 9 if ($b == 9);" - note the "()"!
721 87 100 100     459 return $self->_parse_conditional($element)
722             if (ref($element) eq 'PPI::Statement' && $element->find_any('PPI::Structure::Condition'));
723              
724             # Example: "$a = 9 if $b == 9;" - note the missing "()"!
725 82 100       8035 if (ref($element) eq 'PPI::Statement')
726             {
727 24         75 my $c = $element->find_first('PPI::Token::Word');
728 24 50       10372 return $self->_parse_conditional($element) if $c =~ /^(if|unless)\z/;
729             }
730              
731             # handle normal expressions like:
732             # "$a == 1"
733             # "use strict;"
734 82 100       1009 return $self->_parse_expression($element)
735             if ( $element->isa('PPI::Statement') );
736             # ($element->isa('PPI::Statement::Expression')) ||
737             # ($element->isa('PPI::Statement::Include')) );
738              
739             # recurse into our children, but ignore whitespace, comments, Null (";") etc:
740 42 100       176 if ($element->isa('PPI::Node'))
741             {
742 40         101 foreach my $child (@{$element->{children}})
  40         401  
743             {
744 144 100       29174 $self->_parse($child) if $child->significant();
745             }
746             }
747             }
748              
749             1;
750             __END__