File Coverage

blib/lib/Devel/Cycle.pm
Criterion Covered Total %
statement 107 139 76.9
branch 34 78 43.5
condition 9 18 50.0
subroutine 17 22 77.2
pod 2 2 100.0
total 169 259 65.2


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   22671 use 5.006001;
  1         4  
  1         42  
5 1     1   5 use strict;
  1         2  
  1         114  
6 1     1   5 use Carp 'croak','carp';
  1         6  
  1         129  
7 1     1   7 use warnings;
  1         1  
  1         73  
8              
9 1     1   6 use Scalar::Util qw(isweak blessed refaddr reftype);
  1         1  
  1         231  
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.11';
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   5 require constant;
31             constant->import( HAVE_PADWALKER =>
32 1         1 eval {
33 1         2035 require PadWalker;
34 1         9851 $PadWalker::VERSION >= 1.0;
35             });
36             }
37              
38             sub import {
39 1     1   10 my $self = shift;
40 1         1 my @args = @_;
41 1         3 my %args = map {$_=>1} @args;
  0         0  
42 1 50       6 $QUIET++ if exists $args{-quiet};
43 1 50       3 $FORMATTING = 'roasted' if exists $args{-roasted};
44 1 50       4 $FORMATTING = 'raw' if exists $args{-raw};
45 1 50       12 $FORMATTING = 'cooked' if exists $args{-cooked};
46 1         132 $self->export_to_level(1,$self,grep {!exists $import_args{$_}} @_);
  0         0  
47             }
48              
49             sub find_weakened_cycle {
50 2     2 1 785 my $ref = shift;
51 2         4 my $callback = shift;
52 2 50       7 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         6 _find_cycle($ref,{},$callback,1,{},());
59             }
60              
61             sub find_cycle {
62 8     8 1 3846 my $ref = shift;
63 8         14 my $callback = shift;
64 8 100       23 unless ($callback) {
65 2         4 my $counter = 0;
66             $callback = sub {
67 0     0   0 _do_report(++$counter,shift)
68             }
69 2         8 }
70 8         25 _find_cycle($ref,{},$callback,0,{},());
71             }
72              
73             sub _find_cycle {
74 143     143   175 my $current = shift;
75 143         158 my $seenit = shift;
76 143         141 my $callback = shift;
77 143         147 my $inc_weak_refs = shift;
78 143         144 my $complain = shift;
79 143         371 my @report = @_;
80              
81 143 100       6987 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 60 100       203 if ($seenit->{refaddr $current}) {
92 18         47 $callback->(\@report);
93 18         185 return;
94             }
95 42         117 $seenit->{refaddr $current}++;
96              
97 42         191 _find_cycle_dispatch($current,{%$seenit},$callback,$inc_weak_refs,$complain,@report);
98             }
99              
100             sub _find_cycle_dispatch {
101 44     44   174 my $type = _get_type($_[0]);
102              
103 44 100       100 if (!defined $type) {
104 2         8 my $ref = reftype $_[0];
105 2         3 our %already_warned;
106 2 100       7 if (!$already_warned{$ref}++) {
107 1         15 warn "Unhandled type: $ref";
108             }
109 2         26 return;
110             }
111 1     1   13 my $sub = do { no strict 'refs'; \&{"_find_cycle_$type"} };
  1         2  
  1         1978  
  42         41  
  42         42  
  42         117  
112 42         108 $sub->(@_);
113             }
114              
115             sub _find_cycle_SCALAR {
116 2     2   4 my $current = shift;
117 2         3 my $seenit = shift;
118 2         5 my $callback = shift;
119 2         3 my $inc_weak_refs = shift;
120 2         3 my $complain = shift;
121 2         4 my @report = @_;
122              
123 2 50 33     16 return if !$inc_weak_refs && isweak($$current);
124 2 50       17 _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 19     19   22 my $current = shift;
130 19         20 my $seenit = shift;
131 19         19 my $callback = shift;
132 19         18 my $inc_weak_refs = shift;
133 19         21 my $complain = shift;
134 19         33 my @report = @_;
135              
136 19         48 for (my $i=0; $i<@$current; $i++) {
137 79 100 100     311 next if !$inc_weak_refs && isweak($current->[$i]);
138 77 100       467 _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 20     20   23 my $current = shift;
145 20         25 my $seenit = shift;
146 20         22 my $callback = shift;
147 20         23 my $inc_weak_refs = shift;
148 20         20 my $complain = shift;
149 20         47 my @report = @_;
150              
151 20         122 for my $key (sort keys %$current) {
152 57 100 100     500 next if !$inc_weak_refs && isweak($current->{$key});
153 54 100       704 _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 1     1   2 my $current = shift;
160 1         3 my $seenit = shift;
161 1         1 my $callback = shift;
162 1         3 my $inc_weak_refs = shift;
163 1         2 my $complain = shift;
164 1         2 my @report = @_;
165              
166 1         2 unless (HAVE_PADWALKER) {
167             if (!$complain->{$current} && !$QUIET) {
168             carp "A code closure was detected in but we cannot check it unless the PadWalker module is installed";
169             }
170              
171             return;
172             }
173              
174 1         544 my $closed_vars = PadWalker::closed_over( $current );
175 1         7 foreach my $varname ( sort keys %$closed_vars ) {
176 2         4 my $value = $closed_vars->{$varname};
177 2         13 _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 44     44   60 my $thingy = shift;
217 44 50       118 return unless ref $thingy;
218 44 100 66     269 return 'SCALAR' if UNIVERSAL::isa($thingy,'SCALAR') || UNIVERSAL::isa($thingy,'REF');
219 42 100       128 return 'ARRAY' if UNIVERSAL::isa($thingy,'ARRAY');
220 23 100       82 return 'HASH' if UNIVERSAL::isa($thingy,'HASH');
221 3 100       13 return 'CODE' if UNIVERSAL::isa($thingy,'CODE');
222 2         3 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__