File Coverage

blib/lib/Devel/Cycle.pm
Criterion Covered Total %
statement 95 142 66.9
branch 33 82 40.2
condition 9 21 42.8
subroutine 16 22 72.7
pod 2 2 100.0
total 155 269 57.6


line stmt bran cond sub pod time code
1             package Devel::Cycle;
2             # $Id: Cycle.pm,v 1.15 2009/08/24 12:51:02 lstein Exp $
3              
4 1     1   15536 use 5.006001;
  1         2  
  1         31  
5 1     1   3 use strict;
  1         2  
  1         29  
6 1     1   3 use Carp 'croak','carp';
  1         5  
  1         59  
7 1     1   4 use warnings;
  1         1  
  1         26  
8              
9 1     1   3 use Scalar::Util qw(isweak blessed refaddr reftype);
  1         1  
  1         160  
10              
11             my $SHORT_NAME = 'A';
12             my %SHORT_NAMES;
13              
14              
15             require Exporter;
16              
17             our @ISA = qw(Exporter);
18             our @EXPORT = qw(find_cycle find_weakened_cycle);
19             our @EXPORT_OK = qw($FORMATTING);
20             our $VERSION = '1.12';
21             our $FORMATTING = 'roasted';
22             our $QUIET = 0;
23              
24             my %import_args = (-quiet =>1,
25             -raw =>1,
26             -cooked =>1,
27             -roasted=>1);
28              
29             BEGIN {
30 1     1   4 require constant;
31             constant->import( HAVE_PADWALKER =>
32 1         2 eval {
33 1         611 require PadWalker;
34 0         0 $PadWalker::VERSION >= 1.0;
35             });
36             }
37              
38             sub import {
39 1     1   7 my $self = shift;
40 1         1 my @args = @_;
41 1         2 my %args = map {$_=>1} @args;
  0         0  
42 1 50       3 $QUIET++ if exists $args{-quiet};
43 1 50       2 $FORMATTING = 'roasted' if exists $args{-roasted};
44 1 50       2 $FORMATTING = 'raw' if exists $args{-raw};
45 1 50       3 $FORMATTING = 'cooked' if exists $args{-cooked};
46 1         76 $self->export_to_level(1,$self,grep {!exists $import_args{$_}} @_);
  0         0  
47             }
48              
49             sub find_weakened_cycle {
50 2     2 1 577 my $ref = shift;
51 2         3 my $callback = shift;
52 2 50       5 unless ($callback) {
53 0         0 my $counter = 0;
54             $callback = sub {
55 0     0   0 _do_report(++$counter,shift)
56             }
57 0         0 }
58 2         5 _find_cycle($ref,{},$callback,1,{},());
59             }
60              
61             sub find_cycle {
62 7     7 1 2408 my $ref = shift;
63 7         7 my $callback = shift;
64 7 100       17 unless ($callback) {
65 2         3 my $counter = 0;
66             $callback = sub {
67 0     0   0 _do_report(++$counter,shift)
68             }
69 2         6 }
70 7         17 _find_cycle($ref,{},$callback,0,{},());
71             }
72              
73             sub _find_cycle {
74 135     135   111 my $current = shift;
75 135         92 my $seenit = shift;
76 135         96 my $callback = shift;
77 135         96 my $inc_weak_refs = shift;
78 135         98 my $complain = shift;
79 135         143 my @report = @_;
80              
81 135 100       379 return unless ref $current;
82              
83             # note: it seems like you could just do:
84             #
85             # return if isweak($current);
86             #
87             # but strangely the weak flag doesn't seem to survive the copying,
88             # so the test has to happen directly on the reference in the data
89             # structure being scanned.
90              
91 52 100       121 if ($seenit->{refaddr $current}) {
92 15         28 $callback->(\@report);
93 15         84 return;
94             }
95 37         64 $seenit->{refaddr $current}++;
96              
97 37         99 _find_cycle_dispatch($current,{%$seenit},$callback,$inc_weak_refs,$complain,@report);
98             }
99              
100             sub _find_cycle_dispatch {
101 37     37   51 my $type = _get_type($_[0]);
102              
103 37 100       59 if (!defined $type) {
104 2         4 my $ref = reftype $_[0];
105 2         2 our %already_warned;
106 2 100       8 if (!$already_warned{$ref}++) {
107 1         9 warn "Unhandled type: $ref";
108             }
109 2         17 return;
110             }
111 1     1   5 my $sub = do { no strict 'refs'; \&{"_find_cycle_$type"} };
  1         2  
  1         956  
  35         21  
  35         23  
  35         73  
112 35         57 $sub->(@_);
113             }
114              
115             sub _find_cycle_SCALAR {
116 2     2   3 my $current = shift;
117 2         1 my $seenit = shift;
118 2         3 my $callback = shift;
119 2         2 my $inc_weak_refs = shift;
120 2         1 my $complain = shift;
121 2         3 my @report = @_;
122              
123 2 50 33     39 return if !$inc_weak_refs && isweak($$current);
124 2 50       13 _find_cycle($$current,{%$seenit},$callback,$inc_weak_refs,$complain,
125             (@report,['SCALAR',undef,$current => $$current,$inc_weak_refs?isweak($$current):()]));
126             }
127              
128             sub _find_cycle_ARRAY {
129 15     15   15 my $current = shift;
130 15         9 my $seenit = shift;
131 15         15 my $callback = shift;
132 15         12 my $inc_weak_refs = shift;
133 15         12 my $complain = shift;
134 15         15 my @report = @_;
135              
136 15         466 for (my $i=0; $i<@$current; $i++) {
137 75 100 100     191 next if !$inc_weak_refs && isweak($current->[$i]);
138 73 100       244 _find_cycle($current->[$i],{%$seenit},$callback,$inc_weak_refs,$complain,
139             (@report,['ARRAY',$i,$current => $current->[$i],$inc_weak_refs?isweak($current->[$i]):()]));
140             }
141             }
142              
143             sub _find_cycle_HASH {
144 18     18   14 my $current = shift;
145 18         11 my $seenit = shift;
146 18         16 my $callback = shift;
147 18         15 my $inc_weak_refs = shift;
148 18         9 my $complain = shift;
149 18         39 my @report = @_;
150              
151 18         82 for my $key (sort keys %$current) {
152 54 100 100     162 next if !$inc_weak_refs && isweak($current->{$key});
153 51 100       183 _find_cycle($current->{$key},{%$seenit},$callback,$inc_weak_refs,$complain,
154             (@report,['HASH',$key,$current => $current->{$key},$inc_weak_refs?isweak($current->{$key}):()]));
155             }
156             }
157              
158             sub _find_cycle_CODE {
159 0     0   0 my $current = shift;
160 0         0 my $seenit = shift;
161 0         0 my $callback = shift;
162 0         0 my $inc_weak_refs = shift;
163 0         0 my $complain = shift;
164 0         0 my @report = @_;
165              
166 0 0       0 unless (HAVE_PADWALKER) {
167 0 0 0     0 if (!$complain->{$current} && !$QUIET) {
168 0         0 carp "A code closure was detected in but we cannot check it unless the PadWalker module is installed";
169             }
170              
171 0         0 return;
172             }
173              
174 0         0 my $closed_vars = PadWalker::closed_over( $current );
175 0         0 foreach my $varname ( sort keys %$closed_vars ) {
176 0         0 my $value = $closed_vars->{$varname};
177 0         0 _find_cycle_dispatch($value,{%$seenit},$callback,$inc_weak_refs,$complain,
178             (@report,['CODE',$varname,$current => $value]));
179             }
180             }
181              
182             sub _do_report {
183 0     0   0 my $counter = shift;
184 0         0 my $path = shift;
185 0         0 print "Cycle ($counter):\n";
186 0         0 foreach (@$path) {
187 0         0 my ($type,$index,$ref,$value,$is_weak) = @$_;
188 0 0       0 printf("\t%30s => %-30s\n",($is_weak ? 'w-> ' : '')._format_reference($type,$index,$ref,0),_format_reference(undef,undef,$value,1));
189             }
190 0         0 print "\n";
191             }
192              
193             sub _format_reference {
194 0     0   0 my ($type,$index,$ref,$deref) = @_;
195 0   0     0 $type ||= _get_type($ref);
196 0 0       0 return $ref unless $type;
197 0 0       0 my $suffix = defined $index ? _format_index($type,$index) : '';
198 0 0       0 if ($FORMATTING eq 'raw') {
199 0         0 return $ref.$suffix;
200             }
201              
202             else {
203 0         0 my $package = blessed($ref);
204 0 0       0 my $prefix = $package ? ($FORMATTING eq 'roasted' ? "${package}::" : "${package}=" ) : '';
    0          
205 0 0       0 my $sygil = $deref ? '\\' : '';
206 0   0     0 my $shortname = ($SHORT_NAMES{$ref} ||= $SHORT_NAME++);
207 0 0       0 return $sygil . ($sygil ? '$' : '$$'). $prefix . $shortname . $suffix if $type eq 'SCALAR';
    0          
208 0 0       0 return $sygil . ($sygil ? '@' : '$') . $prefix . $shortname . $suffix if $type eq 'ARRAY';
    0          
209 0 0       0 return $sygil . ($sygil ? '%' : '$') . $prefix . $shortname . $suffix if $type eq 'HASH';
    0          
210 0 0       0 return $sygil . ($sygil ? '&' : '$') . $prefix . $shortname . $suffix if $type eq 'CODE';
    0          
211             }
212             }
213              
214             # why not Scalar::Util::reftype?
215             sub _get_type {
216 37     37   31 my $thingy = shift;
217 37 50       55 return unless ref $thingy;
218 37 100 66     156 return 'SCALAR' if UNIVERSAL::isa($thingy,'SCALAR') || UNIVERSAL::isa($thingy,'REF');
219 35 100       70 return 'ARRAY' if UNIVERSAL::isa($thingy,'ARRAY');
220 20 100       49 return 'HASH' if UNIVERSAL::isa($thingy,'HASH');
221 2 50       4 return 'CODE' if UNIVERSAL::isa($thingy,'CODE');
222 2         4 undef;
223             }
224              
225             sub _format_index {
226 0     0     my ($type,$index) = @_;
227 0 0         return "->[$index]" if $type eq 'ARRAY';
228 0 0         return "->{'$index'}" if $type eq 'HASH';
229 0 0         return " variable $index" if $type eq 'CODE';
230 0           return;
231             }
232              
233             1;
234             __END__