File Coverage

blib/lib/Devel/TraceUse.pm
Criterion Covered Total %
statement 41 117 35.0
branch 13 74 17.5
condition 3 10 30.0
subroutine 8 16 50.0
pod 0 7 0.0
total 65 224 29.0


line stmt bran cond sub pod time code
1             package Devel::TraceUse;
2             $Devel::TraceUse::VERSION = '2.096';
3             # detect being loaded via -d:TraceUse and disable the debugger features we
4             # don't need. better names for evals (0x100) and anon subs (0x200).
5             BEGIN {
6 3 50 33 3   136110 if (!defined &DB::DB && $^P & 0x02) {
7 0         0 $^P = 0x100 | 0x200;
8             }
9             }
10              
11             BEGIN {
12 3     3   38 unshift @INC, \&trace_use;
13             *CORE::GLOBAL::require = sub {
14 65     65   14812 my ($arg) = @_;
15              
16             # ensure our hook remains first in @INC
17 65 100       262 @INC = ( \&trace_use, grep "$_" ne \&trace_use . '', @INC )
18             if $INC[0] ne \&trace_use;
19              
20             # let require do the heavy lifting
21 65         428 CORE::require($arg);
22 3         5789 };
23             }
24              
25             # initialize the tree of require calls
26             my $root = (caller)[1];
27              
28             # keys in %TRACE:
29             # - ranked: modules load attemps in chronological order
30             # - loaded_by: track "filename"s loaded by "filepath" (value from %INC)
31             # - used: track loaded modules by "filename" (parameter to require)
32             # - loader: track potential proxy modules
33             #
34             # %TRACE is built incrementally by trace_use, and augmented by post_process
35             my %TRACE;
36              
37             my %reported; # track reported "filename"
38             my $rank = 0; # record the loading order of modules
39             my $quiet = 1; # no output until decided otherwise
40             my $output_fh; # optional write filehandle where results will be output
41              
42             # Hide core modules (for the specified version)?
43             my $hide_core = 0;
44              
45             sub import {
46 0     0   0 my $class = shift;
47              
48             # ensure "use Devel::TraceUse ();" will produce no output
49 0         0 $quiet = 0;
50              
51             # process options
52 0         0 for(@_) {
53 0 0       0 if(/^hidecore(?::(.*))?/) {
    0          
54 0 0       0 $hide_core = numify( $1 ? $1 : $] );
55             } elsif (/^output:(.*)$/) {
56 0 0       0 open $output_fh, '>', $1 or die "can't open $1: $!";
57             } else {
58 0         0 die "Unknown argument to $class: $_\n";
59             }
60             }
61             }
62              
63             my @caller_info = qw( package filepath line );
64              
65             ### %TRACE CONSTRUCTION
66              
67             # Keys used in the data structure:
68             # - filename: parameter passed to use/require
69             # - module: module, computed from filename
70             # - rank: rank of loading
71             # - eval: was this use/require done in an eval?
72             # - loaded: list of files loaded from this one
73             # - filepath: file that was actually loaded from disk (obtained from %INC)
74             # - caller: information on the caller (same keys + everything from caller())
75              
76             sub trace_use
77             {
78 18     18 0 146867 my ( $code, $filename ) = @_;
79              
80             # $filename may be an actual filename, e.g. with do()
81             # try to compute a module name from it
82 18         29 my $module = $filename;
83 18 50       130 $module =~ s{/}{::}g
84             if $module =~ s/\.pm$//;
85              
86             # chronological list of modules we tried to load
87 18         33 push @{ $TRACE{ranked} }, my $info = {
  18         104  
88             filename => $filename,
89             module => $module,
90             rank => ++$rank,
91             eval => '',
92             };
93              
94             # info about the loading module
95             # (our require override adds one frame)
96 18         42 my $caller = $info->{caller} = {};
97 18         97 @{$caller}{@caller_info} = caller(1);
  18         65  
98              
99             # try to compute a "filename" (as received by require)
100 18         46 $caller->{filename} = $caller->{filepath};
101              
102             # some values seen in the wild:
103             # - "(eval $num)[$path:$line]" (debugger)
104             # - "$filename (autosplit into $path)" (AutoLoader)
105 18 100       58 if ( $caller->{filename} =~ /^(\(eval \d+\))(?:\[(.*):(\d+)\])?$/ ) {
106 2         6 $info->{eval} = $1;
107 2         6 $caller->{filename} = $caller->{filepath} = $2;
108 2         4 $caller->{line} = $3;
109             }
110              
111             # clean up path
112             $caller->{filename}
113 18         31 =~ s!^(?:@{[ join '|', map quotemeta, reverse sort @INC ]})/?!!;
  18         1134  
114              
115             # try to compute the package associated with the file
116 18         77 $caller->{filepackage} = $caller->{filename};
117 18         41 $caller->{filepackage} =~ s/\.(pm|al)\s.*$/.$1/;
118             $caller->{filepackage} =~ s{/}{::}g
119 18 100       85 if $caller->{filepackage} =~ s/\.pm$//;
120              
121             # record who tried to load us (and store our index)
122 18         34 push @{ $TRACE{loaded_by}{ $caller->{filepath} } }, $info->{rank} - 1;
  18         63  
123              
124             # record potential proxies
125 18 50       45 if ( $caller->{filename} ) {
126 18         27 my $level = 1; # our require override adds one frame
127 18         35 my $subroutine;
128 18   100     120 while ( $subroutine = ( caller ++$level )[3] || '' ) {
129 21 100       93 last if $subroutine =~ /::/;
130             }
131 18         35 $TRACE{loader}{ join "\0", @{$caller}{qw( filename line )}, $subroutine }++;
  18         78  
132             }
133              
134             # let Perl ultimately find the required file
135 18         12295 return;
136             }
137              
138             # some post-processing that requires the modules to have been actually loaded
139             sub post_process {
140              
141             # process the list of loading attempts in reverse order:
142             # if a module shows up more than once, then all occurences
143             # are failures to load, except maybe the last one
144 0 0   0 0 0 for my $module ( reverse @{ $TRACE{ranked} || [] } ) {
  0         0  
145 0         0 my $filename = $module->{filename};
146              
147             # module was successfully loaded
148 0 0       0 if ( exists $INC{$filename} ) {
149 0   0     0 $TRACE{used}{$filename} ||= $module;
150             }
151             }
152              
153             # map "filename" to "filepath" for everything that was loaded
154 0         0 while ( my ( $filename, $filepath ) = each %INC ) {
155 0 0       0 if ( exists $TRACE{used}{$filename} ) {
156 0   0     0 $TRACE{used}{$filename}{loaded} = delete $TRACE{loaded_by}{$filepath} || [];
157 0         0 $TRACE{used}{$filename}{filepath} = $filepath;
158             }
159             }
160              
161             # extract version
162 0         0 for my $mod ( @{ $TRACE{ranked} } ) {
  0         0  
163 0         0 $mod->{version} = ${"$mod->{module}\::VERSION"};
  0         0  
164             }
165             }
166              
167             ### UTILITY FUNCTIONS
168              
169             # we don't want to use version.pm on old Perls
170             sub numify {
171 24     24 0 112915 my ($version) = @_;
172 24         52 $version =~ y/_//d;
173 24         82 my @parts = split /\./, $version;
174              
175             # %Module::CoreList::version's keys are x.yyyzzz *numbers*
176 24         208 return 0+ join '', shift @parts, '.', map sprintf( '%03s', $_ ), @parts;
177             }
178              
179             ### OUTPUT FORMATTERS
180              
181             sub show_trace_visitor {
182 0     0 0 0 my ( $mod, $pos, $output_cb, @args ) = @_;
183              
184 0         0 my $caller = $mod->{caller};
185 0         0 my $message = sprintf( '%4s.', $mod->{rank} ) . ' ' x $pos;
186 0         0 $message .= "$mod->{module}";
187 0 0       0 $message .= defined $mod->{version} ? " $mod->{version}," : ',';
188             $message .= " $caller->{filename}"
189 0 0       0 if defined $caller->{filename};
190             $message .= " line $caller->{line}"
191 0 0       0 if defined $caller->{line};
192             $message .= " $mod->{eval}"
193 0 0       0 if $mod->{eval};
194             $message .= " [$caller->{package}]"
195 0 0       0 if $caller->{package} ne $caller->{filepackage};
196             $message .= " (FAILED)"
197 0 0       0 if !exists $mod->{filepath};
198              
199 0         0 $output_cb->($message, @args);
200             }
201              
202             sub visit_trace
203             {
204 0     0 0 0 my ( $visitor, $mod, $pos, @args ) = @_;
205              
206 0         0 my $hide = 0;
207              
208 0 0       0 if ( ref $mod ) {
209 0 0       0 if($hide_core) {
210 0         0 $hide = exists $Module::CoreList::version{$hide_core}{$mod->{module}};
211             }
212 0 0       0 $visitor->( $mod, $pos, @args ) unless $hide;
213 0         0 $reported{$mod->{filename}}++;
214             }
215             else {
216 0         0 $mod = { loaded => delete $TRACE{loaded_by}{$mod} };
217             }
218              
219             visit_trace( $visitor, $_, $hide ? $pos : $pos + 1, @args )
220 0 0       0 for map $TRACE{ranked}[$_], @{ $mod->{loaded} };
  0         0  
221             }
222              
223             sub dump_proxies
224             {
225 0     0 0 0 my $output = shift;
226              
227             my @hot_loaders =
228 0         0 sort { $TRACE{loader}{$b} <=> $TRACE{loader}{$a} }
229 0         0 grep { $TRACE{loader}{$_} > 1 }
230 0         0 keys %{ $TRACE{loader} };
  0         0  
231              
232 0 0       0 return unless @hot_loaders;
233              
234 0         0 $output->("Possible proxies:");
235              
236 0         0 for my $loader (@hot_loaders) {
237 0         0 my ( $filename, $line, $subroutine ) = split /\0/, $loader;
238             $output->(sprintf("%4d %s line %d%s",
239 0 0       0 $TRACE{loader}{$loader},
240             $filename, $line,
241             (length($subroutine) ? ", sub $subroutine" : '')));
242             }
243             }
244              
245             sub dump_result
246             {
247 3 50   3 0 98 return if $quiet;
248              
249 0         0 post_process();
250              
251             # let people know more accurate information is available
252 0 0       0 warn "Use -d:TraceUse for more accurate information.\n" if !$^P;
253              
254             # load Module::CoreList if needed
255 0 0       0 if ($hide_core) {
256 0         0 local @INC = grep { $_ ne \&trace_use } @INC;
  0         0  
257 0         0 local %INC = %INC; # don't report it loaded
258 0     0   0 local *trace_use = sub {};
259 0         0 require Module::CoreList;
260             warn sprintf "Module::CoreList %s doesn't know about Perl %s\n",
261             $Module::CoreList::VERSION, $hide_core
262 0 0       0 if !exists $Module::CoreList::version{$hide_core};
263             }
264              
265             my $output = defined $output_fh
266 0     0   0 ? sub { print $output_fh "$_[0]\n" }
267 0 0   0   0 : sub { warn "$_[0]\n" };
  0         0  
268              
269             # output the diagnostic
270 0         0 $output->("Modules used from $root:");
271 0         0 visit_trace( \&show_trace_visitor, $root, 0, $output );
272              
273             # anything left?
274 0 0       0 if ( %{ $TRACE{loaded_by} } ) {
  0         0  
275             visit_trace( \&show_trace_visitor, $_, 0, $output )
276 0         0 for sort keys %{ $TRACE{loaded_by} };
  0         0  
277             }
278              
279             # did we miss some modules?
280 0 0       0 if (my @missed
281 0 0       0 = sort grep { !exists $reported{$_} && $_ ne 'Devel/TraceUse.pm' }
282             keys %INC
283             )
284             {
285 0 0       0 $output->("Modules used, but not reported:") if @missed;
286 0         0 $output->(" $_") for @missed;
287             }
288              
289 0         0 dump_proxies($output);
290              
291 0 0       0 close $output_fh if defined $output_fh;
292             }
293              
294             ### HOOK INSTALLATION
295              
296             # If perl runs with -c we want to dump
297             CHECK {
298             # "perl -c" ?
299 2 50   2   5156 dump_result() if $^C;
300             }
301              
302 3     3   2913747 END { dump_result() }
303              
304             1;
305              
306             __END__