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   72396 use 5.006; # our
  8         19  
2 8     8   29 use strict;
  8         12  
  8         157  
3 8     8   28 use warnings;
  8         8  
  8         466  
4              
5             package Devel::Isa::Explainer;
6              
7             our $VERSION = '0.002000';
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         9  
  8         171  
14 8     8   3483 use Term::ANSIColor 3.00 ('colored'); # bright_
  8         34816  
  8         1885  
15 8     8   40 use Carp ('croak');
  8         7  
  8         265  
16 8     8   3555 use Package::Stash ();
  8         44211  
  8         158  
17 8     8   3287 use MRO::Compat ();
  8         17232  
  8         416  
18              
19             # Perl critic is broken. This is not a void context.
20             ## no critic (BuiltinFunctions::ProhibitVoidMap)
21 8     8   41 use constant 1.03 ( { map { ( ( sprintf '_E%x', $_ ), ( sprintf ' (id: %s#%d)', __PACKAGE__, $_ ), ) } 1 .. 5 } );
  8         126  
  8         26  
  40         807  
22              
23 8     8   3270 use namespace::clean;
  8         41819  
  8         35  
24              
25 8     8   15437 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 3837 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       20 length $target or croak 'Expected target with non-zero length' . _E3;
59 6 50       14 ref $target and croak 'Expected scalar target' . _E4;
60 6         15 return _pp_key() . _pp_class($target);
61             }
62              
63             # -- no user servicable parts --
64             sub _sub_type {
65 332     332   272 my ($sub) = @_;
66 332 100       684 return 'PRIVATE' if $sub =~ /\A_/sx;
67 208 50       307 return 'TYPE_UTIL' if $sub =~ /\A(is_|assert_|to_)[[:upper:]]/sx;
68 208 100       267 return 'PRIVATE' if $sub =~ /\A[[:upper:]][[:upper:]]/sx;
69 200 50       236 return 'TYPE' if $sub =~ /\A[[:upper:]]/sx;
70 200         450 return 'PUBLIC';
71             }
72              
73             sub _hl_TYPE_UTIL {
74 6 50   6   50 if ( $_[0] =~ /\A([^_]+_)(.*\z)/sx ) {
75 6         31 return colored( \@TYPE_UTIL, $1 ) . colored( \@TYPE, $2 );
76             }
77 0         0 return $_[0];
78             }
79              
80             sub _hl_suffix {
81 214 100   214   4153 return colored( $_[0], $SHADOW_SUFFIX ) if $_[2];
82 185 100       310 return colored( $_[0], $SHADOWED_SUFFIX ) if $_[1];
83 169         469 return q[];
84             }
85              
86 6     6   16 sub _hl_TYPE { return colored( \@TYPE, $_[0] ) }
87              
88             sub _hl_PUBLIC {
89 124 100   124   272 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   187 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   192 return __PACKAGE__->can( '_hl_' . _sub_type( $_[0] ) )->(@_);
100             }
101              
102             sub _pp_key {
103 6     6   6 my @tokens;
104 6         15 push @tokens, 'Public Sub: ' . _hl_PUBLIC('foo_example');
105 6         18 push @tokens, 'Type Constraint: ' . _hl_TYPE('TypeName');
106 6         119 push @tokens, 'Type Constraint Utility: ' . _hl_TYPE_UTIL('typeop_TypeName');
107 6         231 push @tokens, 'Private/Boring Sub: ' . _hl_PRIVATE('foo_example');
108 6 50       16 if ($SHOW_SHADOWED) {
109 6         11 push @tokens, 'Public Sub shadowing another: ' . _hl_PUBLIC( 'shadowing_example', 0, 1 );
110 6         106 push @tokens, 'Public Sub shadowed by higher scope: ' . _hl_PUBLIC( 'shadowed_example', 1 );
111 6         99 push @tokens, 'Public Sub shadowing another and shadowed itself: ' . _hl_PUBLIC( 'shadowed_shadowing_example', 1, 1 );
112              
113 6         99 push @tokens, 'Private/Boring Sub shadowing another: ' . _hl_PRIVATE( 'shadowing_example', 0, 1 );
114 6         102 push @tokens, 'Private/Boring Sub shadowed by higher scope: ' . _hl_PRIVATE( 'shadowed_example', 1 );
115 6         99 push @tokens, 'Private/Boring Sub another and shadowed itself: ' . _hl_PRIVATE( 'shadowing_shadowed_example', 1, 1 );
116             }
117 6         95 push @tokens, 'No Subs: ()';
118 6         51 return sprintf "Key:\n$INDENT%s\n\n", join qq[\n$INDENT], @tokens;
119             }
120              
121             sub _mg_sorted {
122 12     12   39 my (%subs) = @_;
123 12 50       21 if ($SHOW_SHADOWED) {
124 12         53 return ( [ sort { lc $a cmp lc $b } keys %subs ] );
  593         443  
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         9 my %clusters;
147 8         24 for my $sub ( keys %subs ) {
148 166         167 $clusters{ _sub_type($sub) }{$sub} = $subs{$sub};
149             }
150 8         11 my @out;
151 8         10 for my $type (qw( PUBLIC PRIVATE TYPE TYPE_UTIL )) {
152 32 100       55 next unless exists $clusters{$type};
153 12         11 push @out, _mg_sorted( %{ $clusters{$type} } );
  12         37  
154             }
155 8         36 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   29 my (%subs) = @_;
174 8         73 my (@clusters) = __PACKAGE__->can( '_mg_' . $CLUSTERING )->(%subs);
175 8         14 my (@out_clusters);
176 8         12 for my $cluster (@clusters) {
177 12         12 my $cluster_out = q[];
178              
179 12         9 my @subs = @{$cluster};
  12         27  
180 12         23 while (@subs) {
181 41         29 my $line = $INDENT;
182             flowcontrol: {
183 41         27 my $sub = shift @subs;
  166         97  
184 166         132 $line .= $sub . q[, ];
185 166 100 100     399 redo flowcontrol if @subs and length $line < $MAX_WIDTH;
186             }
187 41         72 $cluster_out .= "$line\n";
188             }
189              
190             # Suck up trailing ,
191 12         49 $cluster_out =~ s/,[ ]\n\z/\n/sx;
192 12         46 $cluster_out =~ s{(\w+)}{ _pp_sub($1, $subs{$1}->{shadowed}, $subs{$1}->{shadowing} ) }gsex;
  166         395  
193 12         129 push @out_clusters, $cluster_out;
194             }
195 8         53 return join qq[\n], @out_clusters;
196             }
197              
198             sub _pp_class {
199 6     6   7 my ($class) = @_;
200 6         9 my $out = q[];
201 6         12 my $mro_order = _extract_mro($class);
202 4         6 for my $mro_entry ( @{$mro_order} ) {
  4         6  
203 8         26 $out .= colored( ['green'], $mro_entry->{class} ) . q[:];
204 8 50       153 my (%subs) = %{ $mro_entry->{subs} || {} };
  8         71  
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         29 $out .= _pp_subs(%subs) . "\n";
211              
212 8         25 next;
213             }
214 4         48 return $out;
215             }
216              
217             sub _extract_mro {
218 9     9   3742 my ($class) = @_;
219 9         15 my ($seen_subs) = {};
220              
221             ## no critic (ProhibitCallsToUnexportedSubs)
222 9         10 my (@isa) = @{ mro::get_linear_isa($class) };
  9         87  
223              
224             # Discover all subs and compute full MRO every time a new sub-name
225             # is found
226 9         18 for my $isa (@isa) {
227 19         325 for my $sub ( Package::Stash->new($isa)->list_all_symbols('CODE') ) {
228 178 100       241 next if exists $seen_subs->{$sub};
229              
230             # Compute the full sub->package MRO table bottom up
231              
232 167         203 $seen_subs->{$sub} = [];
233 167         93 my $currently_visible;
234 167         136 for my $class ( reverse @isa ) {
235 285 100       797 my $coderef = $class->can($sub) or next;
236              
237             # Record the frame where the first new instance is seen.
238 204 100 100     366 if ( not defined $currently_visible or $currently_visible != $coderef ) {
239 177         105 unshift @{ $seen_subs->{$sub} }, $class; # note: we're working bottom-up
  177         224  
240 177         104 $currently_visible = $coderef;
241 177         195 next;
242             }
243             }
244             }
245             }
246              
247 8         13 my $class_data = {};
248              
249             # Group "seen subs" into class oriented structures,
250             # and classify them.
251 8         12 for my $sub ( keys %{$seen_subs} ) {
  8         38  
252 167         100 my @classes = @{ $seen_subs->{$sub} };
  167         191  
253              
254 167         109 for my $isa ( @{ $seen_subs->{$sub} } ) {
  167         161  
255              
256             # mark all subs both shadowing and shadowed until proven otherwise
257 177         323 $class_data->{$isa}->{$sub} = { shadowed => 1, shadowing => 1 };
258             }
259              
260             # mark top-most sub unshadowed
261 167         159 $class_data->{ $classes[0] }->{$sub}->{shadowed} = 0;
262              
263             # mark bottom-most sub unshadowing
264 167         181 $class_data->{ $classes[-1] }->{$sub}->{shadowing} = 0;
265              
266             }
267              
268             # Order class structures by MRO order
269 8   100     20 my (@mro_order) = map { { class => $_, subs => $class_data->{$_} || {} } } @isa;
  18         71  
270              
271 8 100 100     45 if ( 1 > @mro_order or ( 1 >= @mro_order and 1 > keys %{ $mro_order[0]->{subs} } ) ) {
  3   33     15  
272              
273             # Huh, No inheritance, and no subs. K.
274 1         2 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         137 croak "No module called $class successfully loaded" . _E5;
282             }
283             }
284 7         49 return \@mro_order;
285             }
286              
287             1;
288              
289             __END__