File Coverage

blib/lib/Devel/Isa/Explainer.pm
Criterion Covered Total %
statement 147 174 84.4
branch 36 50 72.0
condition 12 14 85.7
subroutine 25 27 92.5
pod 1 1 100.0
total 221 266 83.0


line stmt bran cond sub pod time code
1 8     8   72256 use 5.006; # our
  8         22  
2 8     8   27 use strict;
  8         7  
  8         133  
3 8     8   21 use warnings;
  8         10  
  8         418  
4              
5             package Devel::Isa::Explainer;
6              
7             our $VERSION = '0.002001';
8              
9             # ABSTRACT: Pretty Print Hierarchies of Subs in Packages
10              
11             our $AUTHORITY = 'cpan:KENTNL'; # AUTHORITY
12              
13 8     8   28 use Exporter ();
  8         7  
  8         163  
14 8     8   3284 use Term::ANSIColor 3.00 ('colored'); # bright_
  8         34754  
  8         2016  
15 8     8   46 use Carp ('croak');
  8         8  
  8         288  
16 8     8   3716 use Package::Stash ();
  8         42745  
  8         143  
17 8     8   3095 use MRO::Compat ();
  8         16105  
  8         379  
18              
19             # Perl critic is broken. This is not a void context.
20             ## no critic (BuiltinFunctions::ProhibitVoidMap)
21 8     8   36 use constant 1.03 ( { map { ( ( sprintf '_E%x', $_ ), ( sprintf ' (id: %s#%d)', __PACKAGE__, $_ ), ) } 1 .. 5 } );
  8         114  
  8         23  
  40         796  
22              
23 8     8   3292 use namespace::clean;
  8         40367  
  8         34  
24              
25 8     8   15630 BEGIN { *import = \&Exporter::import } ## no critic (ProhibitCallsToUnexportedSubs)
26              
27             our @EXPORT_OK = qw( explain_isa );
28              
29             # These exist for twiddling, but are presently undocumented as their interface
30             # is not deemed even remotely stable. Use at own risk.
31              
32             our @TYPE_UTIL = qw( cyan );
33             our @TYPE = qw( yellow );
34             our @PRIVATE = qw( reset );
35             our @PUBLIC = qw( bold bright_green );
36             our @SHADOWED_PRIVATE = qw( magenta );
37             our @SHADOWED_PUBLIC = qw( red );
38              
39             our $MAX_WIDTH = 80;
40             our $SHOW_SHADOWED = 1;
41             our $INDENT = q[ ] x 4;
42             our $SHADOW_SUFFIX = q{(^)};
43             our $SHADOWED_SUFFIX = q{}; # TBD
44             our $CLUSTERING = 'type_clustered';
45              
46              
47              
48              
49              
50              
51              
52              
53              
54             sub explain_isa {
55 6     6 1 3826 my $nargs = scalar( my ( $target, ) = @_ );
56 6 50       17 1 == $nargs or croak "Passed $nargs arguments, Expected 1" . _E1;
57 6 50       14 defined $target or croak 'Expected defined target' . _E2;
58 6 50       19 length $target or croak 'Expected target with non-zero length' . _E3;
59 6 50       12 ref $target and croak 'Expected scalar target' . _E4;
60 6         16 return _pp_key() . _pp_class($target);
61             }
62              
63             # -- no user servicable parts --
64             sub _sub_type {
65 332     332   251 my ($sub) = @_;
66 332 100       658 return 'PRIVATE' if $sub =~ /\A_/sx;
67 208 50       289 return 'TYPE_UTIL' if $sub =~ /\A(is_|assert_|to_)[[:upper:]]/sx;
68 208 100       285 return 'PRIVATE' if $sub =~ /\A[[:upper:]][[:upper:]]/sx;
69 200 50       250 return 'TYPE' if $sub =~ /\A[[:upper:]]/sx;
70 200         401 return 'PUBLIC';
71             }
72              
73             sub _hl_TYPE_UTIL {
74 6 50   6   44 if ( $_[0] =~ /\A([^_]+_)(.*\z)/sx ) {
75 6         15 return colored( \@TYPE_UTIL, $1 ) . colored( \@TYPE, $2 );
76             }
77 0         0 return $_[0];
78             }
79              
80             sub _hl_suffix {
81 214 100   214   4270 return colored( $_[0], $SHADOW_SUFFIX ) if $_[2];
82 185 100       310 return colored( $_[0], $SHADOWED_SUFFIX ) if $_[1];
83 169         458 return q[];
84             }
85              
86 6     6   15 sub _hl_TYPE { return colored( \@TYPE, $_[0] ) }
87              
88             sub _hl_PUBLIC {
89 124 100   124   258 return ( $_[1] ? colored( \@SHADOWED_PUBLIC, $_[0] ) : colored( \@PUBLIC, $_[0] ) )
90             . _hl_suffix( \@SHADOWED_PUBLIC, $_[1], $_[2] );
91             }
92              
93             sub _hl_PRIVATE {
94 90 100   90   177 return ( $_[1] ? colored( \@SHADOWED_PRIVATE, $_[0] ) : colored( \@PRIVATE, $_[0] ) )
95             . _hl_suffix( \@SHADOWED_PRIVATE, $_[1], $_[2] );
96             }
97              
98             sub _pp_sub {
99 166     166   191 return __PACKAGE__->can( '_hl_' . _sub_type( $_[0] ) )->(@_);
100             }
101              
102             sub _pp_key {
103 6     6   7 my @tokens;
104 6         14 push @tokens, 'Public Sub: ' . _hl_PUBLIC('foo_example');
105 6         19 push @tokens, 'Type Constraint: ' . _hl_TYPE('TypeName');
106 6         114 push @tokens, 'Type Constraint Utility: ' . _hl_TYPE_UTIL('typeop_TypeName');
107 6         221 push @tokens, 'Private/Boring Sub: ' . _hl_PRIVATE('foo_example');
108 6 50       16 if ($SHOW_SHADOWED) {
109 6         12 push @tokens, 'Public Sub shadowing another: ' . _hl_PUBLIC( 'shadowing_example', 0, 1 );
110 6         126 push @tokens, 'Public Sub shadowed by higher scope: ' . _hl_PUBLIC( 'shadowed_example', 1 );
111 6         100 push @tokens, 'Public Sub shadowing another and shadowed itself: ' . _hl_PUBLIC( 'shadowed_shadowing_example', 1, 1 );
112              
113 6         112 push @tokens, 'Private/Boring Sub shadowing another: ' . _hl_PRIVATE( 'shadowing_example', 0, 1 );
114 6         99 push @tokens, 'Private/Boring Sub shadowed by higher scope: ' . _hl_PRIVATE( 'shadowed_example', 1 );
115 6         96 push @tokens, 'Private/Boring Sub another and shadowed itself: ' . _hl_PRIVATE( 'shadowing_shadowed_example', 1, 1 );
116             }
117 6         92 push @tokens, 'No Subs: ()';
118 6         49 return sprintf "Key:\n$INDENT%s\n\n", join qq[\n$INDENT], @tokens;
119             }
120              
121             sub _mg_sorted {
122 12     12   38 my (%subs) = @_;
123 12 50       18 if ($SHOW_SHADOWED) {
124 12         54 return ( [ sort { lc $a cmp lc $b } keys %subs ] );
  581         432  
125             }
126 0         0 return ( [ grep { !$subs{$_} } sort { lc $a cmp lc $b } keys %subs ] );
  0         0  
  0         0  
127             }
128              
129             sub _mg_type_shadow_clustered {
130 0     0   0 my (%subs) = @_;
131 0         0 my %clusters;
132 0         0 for my $sub ( keys %subs ) {
133 0         0 my $shadow = '.shadowed' x !!$subs{$sub};
134 0         0 $clusters{ _sub_type($sub) . $shadow }{$sub} = $subs{$sub};
135             }
136 0         0 my @out;
137 0         0 for my $type ( map { ( $_, "$_.shadowed" ) } qw( PUBLIC PRIVATE TYPE TYPE_UTIL ) ) {
  0         0  
138 0 0       0 next unless exists $clusters{$type};
139 0         0 push @out, _mg_sorted( %{ $clusters{$type} } );
  0         0  
140             }
141 0         0 return @out;
142             }
143              
144             sub _mg_type_clustered {
145 8     8   32 my (%subs) = @_;
146 8         7 my %clusters;
147 8         29 for my $sub ( keys %subs ) {
148 166         152 $clusters{ _sub_type($sub) }{$sub} = $subs{$sub};
149             }
150 8         14 my @out;
151 8         8 for my $type (qw( PUBLIC PRIVATE TYPE TYPE_UTIL )) {
152 32 100       51 next unless exists $clusters{$type};
153 12         10 push @out, _mg_sorted( %{ $clusters{$type} } );
  12         36  
154             }
155 8         35 return @out;
156             }
157              
158             sub _mg_aleph {
159 0     0   0 my (%subs) = @_;
160 0         0 my %clusters;
161 0         0 for my $sub ( keys %subs ) {
162 0         0 $clusters{ lc( substr $sub, 0, 1 ) }{$sub} = $subs{$sub};
163             }
164 0         0 my @out;
165 0         0 for my $key ( sort keys %clusters ) {
166 0         0 push @out, _mg_sorted( %{ $clusters{$key} } );
  0         0  
167             }
168 0         0 return @out;
169              
170             }
171              
172             sub _pp_subs {
173 8     8   26 my (%subs) = @_;
174 8         71 my (@clusters) = __PACKAGE__->can( '_mg_' . $CLUSTERING )->(%subs);
175 8         14 my (@out_clusters);
176 8         9 for my $cluster (@clusters) {
177 12         26 my $cluster_out = q[];
178              
179 12         12 my @subs = @{$cluster};
  12         25  
180 12         23 while (@subs) {
181 41         27 my $line = $INDENT;
182             flowcontrol: {
183 41         29 my $sub = shift @subs;
  166         97  
184 166         123 $line .= $sub . q[, ];
185 166 100 100     421 redo flowcontrol if @subs and length $line < $MAX_WIDTH;
186             }
187 41         75 $cluster_out .= "$line\n";
188             }
189              
190             # Suck up trailing ,
191 12         47 $cluster_out =~ s/,[ ]\n\z/\n/sx;
192 12         41 $cluster_out =~ s{(\w+)}{ _pp_sub($1, $subs{$1}->{shadowed}, $subs{$1}->{shadowing} ) }gsex;
  166         370  
193 12         124 push @out_clusters, $cluster_out;
194             }
195 8         60 return join qq[\n], @out_clusters;
196             }
197              
198             sub _pp_class {
199 6     6   7 my ($class) = @_;
200 6         7 my $out = q[];
201 6         13 my $mro_order = _extract_mro($class);
202 4         6 for my $mro_entry ( @{$mro_order} ) {
  4         8  
203 8         23 $out .= colored( ['green'], $mro_entry->{class} ) . q[:];
204 8 50       146 my (%subs) = %{ $mro_entry->{subs} || {} };
  8         65  
205 8 50       20 if ( not keys %subs ) {
206 0         0 $out .= " ()\n";
207 0         0 next;
208             }
209 8         10 else { $out .= "\n" }
210 8         28 $out .= _pp_subs(%subs) . "\n";
211              
212 8         24 next;
213             }
214 4         49 return $out;
215             }
216              
217             sub _extract_mro {
218 9     9   3149 my ($class) = @_;
219 9         15 my ($seen_subs) = {};
220              
221             ## no critic (ProhibitCallsToUnexportedSubs)
222 9         8 my (@isa) = @{ mro::get_linear_isa($class) };
  9         73  
223              
224             # Discover all subs and compute full MRO every time a new sub-name
225             # is found
226 9         14 for my $isa (@isa) {
227 19         290 for my $sub ( Package::Stash->new($isa)->list_all_symbols('CODE') ) {
228 178 100       246 next if exists $seen_subs->{$sub};
229              
230             # Compute the full sub->package MRO table bottom up
231              
232 167         179 $seen_subs->{$sub} = [];
233 167         92 my $currently_visible;
234 167         132 for my $class ( reverse @isa ) {
235 285 100       767 my $coderef = $class->can($sub) or next;
236              
237             # Record the frame where the first new instance is seen.
238 204 100 100     390 if ( not defined $currently_visible or $currently_visible != $coderef ) {
239 177         114 unshift @{ $seen_subs->{$sub} }, $class; # note: we're working bottom-up
  177         211  
240 177         112 $currently_visible = $coderef;
241 177         188 next;
242             }
243             }
244             }
245             }
246              
247 8         12 my $class_data = {};
248              
249             # Group "seen subs" into class oriented structures,
250             # and classify them.
251 8         8 for my $sub ( keys %{$seen_subs} ) {
  8         31  
252 167         99 my @classes = @{ $seen_subs->{$sub} };
  167         165  
253              
254 167         106 for my $isa ( @{ $seen_subs->{$sub} } ) {
  167         149  
255              
256             # mark all subs both shadowing and shadowed until proven otherwise
257 177         275 $class_data->{$isa}->{$sub} = { shadowed => 1, shadowing => 1 };
258             }
259              
260             # mark top-most sub unshadowed
261 167         135 $class_data->{ $classes[0] }->{$sub}->{shadowed} = 0;
262              
263             # mark bottom-most sub unshadowing
264 167         204 $class_data->{ $classes[-1] }->{$sub}->{shadowing} = 0;
265              
266             }
267              
268             # Order class structures by MRO order
269 8   100     17 my (@mro_order) = map { { class => $_, subs => $class_data->{$_} || {} } } @isa;
  18         59  
270              
271 8 100 100     40 if ( 1 > @mro_order or ( 1 >= @mro_order and 1 > keys %{ $mro_order[0]->{subs} } ) ) {
  3   33     14  
272              
273             # Huh, No inheritance, and no subs. K.
274 1         1 my $module_path = $class;
275 1         7 $module_path =~ s{ (::|') }{/}sgx;
276              
277             # TODO: Maybe don't make this fatal and return data context to user instead?
278             # Undecided, and will have to come after more complex concerns.
279             # - kentnl, Apr 2016
280 1 50       5 if ( not $INC{ $module_path . '.pm' } ) {
281 1         139 croak "No module called $class successfully loaded" . _E5;
282             }
283             }
284 7         43 return \@mro_order;
285             }
286              
287             1;
288              
289             __END__