File Coverage

blib/lib/App/PrereqGrapher.pm
Criterion Covered Total %
statement 65 97 67.0
branch 17 46 36.9
condition 8 18 44.4
subroutine 11 15 73.3
pod 1 5 20.0
total 102 181 56.3


line stmt bran cond sub pod time code
1             package App::PrereqGrapher;
2             $App::PrereqGrapher::VERSION = '0.12';
3             #
4             # ABSTRACT: generate dependency graph using Perl::PrereqScanner
5             #
6              
7 1     1   23219 use 5.006;
  1         5  
8 1     1   6 use strict;
  1         1  
  1         25  
9 1     1   18 use warnings;
  1         6  
  1         38  
10              
11 1     1   6 use Carp;
  1         2  
  1         87  
12 1     1   980 use Moo;
  1         15419  
  1         5  
13 1     1   2371 use Perl::PrereqScanner;
  1         5260959  
  1         49  
14 1     1   1359 use Getopt::Long qw/:config no_ignore_case/;
  1         11378  
  1         5  
15 1     1   1477 use Graph::Easy;
  1         126816  
  1         48  
16 1     1   820 use Module::Path qw(module_path);
  1         642  
  1         67  
17 1     1   3800 use Module::CoreList;
  1         52874  
  1         16  
18              
19             my %formats =
20             (
21             'dot' => sub { $_[0]->as_graphviz; },
22             'svg' => sub { $_[0]->as_svg; },
23             'gml' => sub { $_[0]->as_graphml; },
24             'vcg' => sub { $_[0]->as_vcg; },
25             'html' => sub { $_[0]->as_html_file; },
26             );
27              
28             has format => (
29             is => 'ro',
30             isa => sub { croak "valid formats: ", join(", ", keys %formats), "\n" unless exists $formats{$_[0]}; },
31             default => sub { return 'dot'; },
32             );
33              
34             has output_file => (
35             is => 'ro',
36             );
37              
38             has no_core => (
39             is => 'ro',
40             );
41              
42             has no_recurse_core => (
43             is => 'ro',
44             );
45              
46             has verbose => (
47             is => 'ro',
48             );
49              
50             has depth => (
51             is => 'ro',
52             isa => sub { croak "depth must be an integer\n" unless $_[0] =~ /^\d+$/; },
53             );
54              
55             has timeout => (
56             is => 'ro',
57             isa => sub { croak "timeout must be an integer\n" unless $_[0] =~ /^\d+$/; },
58             );
59              
60             sub new_with_options
61             {
62 0     0 0 0 my $class = shift;
63 0         0 my %options = $class->parse_options();
64 0         0 my $instance = $class->new(%options, @_);
65              
66 0         0 return $instance;
67             }
68              
69             sub parse_options
70             {
71 0     0 0 0 my $class = shift;
72 0         0 my %options;
73             my %format;
74              
75             GetOptions(
76             'h|help' => \$options{'help'},
77             'd|depth=i' => \$options{'depth'},
78             't|timeout=i' => \$options{'timeout'},
79             'o|output-file=s' => \$options{'output_file'},
80             'nc|no-core' => \$options{'no_core'},
81             'nrc|no-recurse-core' => \$options{'no_recurse_core'},
82             'v|verbose' => \$options{'verbose'},
83             'dot' => \$format{'dot'},
84             'svg' => \$format{'svg'},
85             'gml' => \$format{'gml'},
86             'vcg' => \$format{'vcg'},
87 0 0       0 'html' => \$format{'html'},
88             ) || croak "Can't get options.";
89 0 0       0 usage() if $options{'help'};
90              
91 0         0 foreach my $format (keys %formats) {
92 0 0       0 delete $format{$format} unless $format{$format};
93             }
94 0 0       0 if (keys %format > 1) {
95 0         0 print "FORMAT: ", join(', ', keys %format), "\n";
96 0         0 croak "you can only specify at most ONE output format (default is 'dot')";
97             }
98 0 0       0 $format{dot} = 1 unless keys %format == 1;
99              
100 0 0 0     0 if ($options{no_core} && $options{no_recurse_core}) {
101 0         0 croak "doesn't make sense to specify no-core and no-recurse-core together";
102             }
103              
104 0         0 for (keys %options) {
105 0 0       0 delete $options{$_} unless defined $options{$_};
106             }
107 0         0 $options{format} = (keys %format)[0];
108              
109 0         0 return %options;
110             }
111              
112             sub generate_graph
113             {
114 2     2 1 60 my ($self, @inputs) = @_;
115 2         5 my (@queue, %seen, $scanner, $graph, $module);
116 0         0 my ($prereqs, $depsref);
117 0         0 my ($path, $filename, $fh);
118 2         4 my $module_count = 0;
119 2         4 my %depth;
120              
121 2         26 $scanner = Perl::PrereqScanner->new;
122 2         65504 $graph = Graph::Easy->new();
123 2         206 $graph->timeout($self->timeout);
124              
125 2         14 @depth{@inputs} = map { 0 } @inputs;
  2         10  
126              
127 2         6 push(@queue, @inputs);
128 2         9 while (@queue > 0) {
129 63         177 $module = shift @queue;
130 63 100       226 next if $seen{$module};
131 23         57 $seen{$module} = 1;
132              
133 23 100       142 if (defined($path = module_path($module))) {
    50          
134             } elsif (-f $module) {
135 0         0 $path = $module;
136             } else {
137 1 50       363791 carp "can't find $module - keeping calm and carrying on.\n" if $self->verbose;
138 1         13 next;
139             }
140              
141             # Huge files (eg currently Perl::Tidy) will cause PPI to barf
142             # So we need to catch those, keep calm, and carry on
143 22         31172 eval { $prereqs = $scanner->scan_file($path); };
  22         137  
144 22 50       19405912 next if $@;
145 22         63 ++$module_count;
146 22         145 $depsref = $prereqs->as_string_hash();
147 22         1476 foreach my $dep (keys %{ $depsref }) {
  22         101  
148 81 100 66     1192 if (!exists($depth{$dep}) || $depth{$dep} > $depth{$module} + 1) {
149 31         79 $depth{$dep} = $depth{$module} + 1;
150             }
151 81 50 33     461 if ($self->no_core && is_core($dep)) {
    100          
152             # don't include core modules
153             } elsif ($dep eq 'perl') {
154 8         60 $graph->add_edge($module, "perl $depsref->{perl}");
155             } else {
156 73         304 $graph->add_edge($module, $dep);
157 73 100 100     7196 next if $self->depth && $depth{$dep} >= $self->depth;
158 61 50 33     342 push(@queue, $dep) unless $self->no_recurse_core && is_core($dep);
159             }
160             }
161             }
162              
163 2   33     13 $filename = $self->output_file || 'dependencies.'.$self->format;
164 2 50       351 open($fh, '>', $filename) ||
165             croak "Failed to write $filename: $!\n";
166 2         65 print $fh $formats{$self->format}->($graph);
167 2         179763 close($fh);
168 2 50       64 print STDERR "$module_count modules processed. Graph written to $filename\n" if $self->verbose;
169             }
170              
171             sub is_core
172             {
173 0     0 0   my $module = shift;
174 0 0         my $version = @_ > 0 ? shift : $^V;
175              
176 0 0         return 0 unless defined(my $first_release = Module::CoreList::first_release($module));
177 0 0         return 0 unless $version >= $first_release;
178 0 0         return 1 if !defined(my $final_release = Module::CoreList::removed_from($module));
179 0           return $version <= $final_release;
180             }
181              
182             sub usage
183             {
184 0     0 0   require Pod::Usage;
185 0           Pod::Usage::pod2usage();
186 0           exit;
187             }
188              
189             1;
190              
191             =head1 NAME
192              
193             App::PrereqGrapher - generate dependency graph using Perl::PrereqScanner
194              
195             =head1 SYNOPSIS
196              
197             use App::PrereqGrapher;
198            
199             my %options = (
200             format => 'dot',
201             no_core => 0,
202             no_recurse_core => 1,
203             output_file => 'prereqs.dot',
204             verbose => 0,
205             );
206             my $grapher = App::PrereqGrapher->new( %options );
207            
208             $grapher->generate_graph('Module::Path');
209              
210             =head1 DESCRIPTION
211              
212             App::PrereqGrapher builds a directed graph of the prereqs or dependencies for
213             a file or module. It uses Perl::PrereqScanner to find the dependencies for the seed,
214             and then repeatedly calls Perl::PrereqScanner on those dependencies, and so on,
215             until all dependencies have been found.
216              
217             It then saves the resulting graph to a file, using one of the five supported formats.
218             The default format is 'dot', the format used by the GraphViz graph drawing toolkit.
219              
220             If your code contains lines like:
221              
222             require 5.006;
223             use 5.006;
224              
225             Then you'll end up with a dependency labelled B<perl 5.006>;
226             this way you can see where you're dependent on modules which
227             require different minimum versions of perl.
228              
229             =head1 METHODS
230              
231             =head2 new
232              
233             The constructor understands the following options:
234              
235             =over 4
236              
237             =item format
238              
239             Select the output format, which must be one of: dot, svg, vcg, gml, or html.
240             See L</"OUTPUT FORMATS"> for more about the supported output formats.
241             If not specified, the default format is 'dot'.
242              
243             =item output_file
244              
245             Specifies the name of the file to write the dependency graph into,
246             including the extension. If not specified, the filename will be C<dependencies>,
247             with the extension set according to the format.
248              
249             =item depth
250              
251             Only generate the graph to the specified depth.
252             If the complete dependency graph is very large, this option may help you get
253             an overview.
254              
255             =item no_core
256              
257             Don't include any modules which are core (included with perl) for the version
258             of perl being used.
259              
260             =item no_recurse_core
261              
262             When a core module is used, include it in the dependency graph,
263             but don't show any of I<its> dependencies.
264              
265             =item timeout
266              
267             Specifies the timeout for Graph::Easy when its laying out the graph.
268             This is mainly relevant for formats like SVG and HTML, where the graph
269             layout is done in Perl. Defaults to 5 seconds.
270              
271             =item verbose
272              
273             Display verbose logging as the grapher runs.
274             Currently this will just tell you if a module was use'd or require'd,
275             but couldn't be found locally.
276              
277             =back
278              
279             =head2 generate_graph
280              
281             Takes one or more seed items. Each item may be a module or the path to a perl file.
282              
283             $grapher->generate_graph('Module::Path', 'Module::Version');
284              
285             It will first try and interpret each item as a module, but if it can't find a module
286             with the given name, it will try and interpret it as a file path.
287             This means that if you have a file called C<strict> for example, then you won't be
288             able to run:
289              
290             $grapher->generate_graph('strict');
291              
292             as it will be interpreted as the module of that name. Put an explicit path to stop this:
293              
294             $grapher->generate_graph('./strict');
295              
296             =head1 OUTPUT FORMATS
297              
298             =over 4
299              
300             =item dot
301              
302             The format used by GraphViz and related tools.
303              
304             =item svg
305              
306             Scalable Vector Graphics (SVG) a W3C standard.
307             You have to install L<Graph::Easy::As_svg> if you want to use this format.
308              
309             =item vcg
310              
311             The VCG or GDL format.
312              
313             =item gml
314              
315             Graph Markup Language, aka GraphML.
316              
317             =item html
318              
319             Generate an HTML format with embedded CSS. I haven't been able to get this to work,
320             but it's one of the formats supported by L<Graph::Easy>.
321              
322             =back
323              
324             =head1 KNOWN BUGS
325              
326             L<Perl::PrereqScanner> uses L<PPI> to parse each item.
327             PPI has a hard-coded limit for the size of file it's prepared to parse
328             (currently just over 1M).
329             This means that very large files will be ignored;
330             for example Perl::Tidy cannot be graphed,
331             and if you try and graph a file that use's Perl::Tidy,
332             then it just won't appear in the graph.
333              
334             If a class isn't defined in its own file,
335             then App::PrereqGrapher won't find it;
336             for example Tie::StdHash is defined inside Tie::Hash.
337             By default these are silently ignored,
338             but if you use the B<-verbose> option you'll get the following warning:
339              
340             can't find Tie::StdHash - keeping calm and carrying on.
341              
342             Perl::PrereqScanner parses code and makes no attempt to
343             determine whether any of it would actually run on your platform.
344             For example, one module might decide at run-time whether to C<require>
345             Foo::Bar or Foo::Baz, and might never use Foo::Baz on your OS.
346             But Perl::PrereqScanner will see both of Foo::Bar and Foo::Baz
347             as pre-reqs.
348              
349             =head1 TODO
350              
351             =over 4
352              
353             =item *
354              
355             Have an option to control what depth we should recurse to?
356             You might only be interested in the dependencies of your code,
357             and their first level of dependencies.
358              
359             =item *
360              
361             Show some indication that we're running. It can take a long time to run
362             if your ultimate dependency graph is very large.
363              
364             =back
365              
366             =head1 SEE ALSO
367              
368             The distribution for this module contains a command-line script,
369             L<prereq-grapher>. It has its own documentation.
370              
371             This module uses L<Perl::PrereqScanner> to parse the source code,
372             and L<Graph::Easy> to generate and save the dependency graph.
373              
374             L<http://neilb.org/reviews/dependencies.html>: a review of CPAN modules that can be used
375             to get dependency information.
376              
377             =head1 REPOSITORY
378              
379             L<https://github.com/neilb/App-PrereqGrapher>
380              
381             =head1 AUTHOR
382              
383             Neil Bowers E<lt>neilb@cpan.orgE<gt>
384              
385             =head1 COPYRIGHT AND LICENSE
386              
387             This software is copyright (c) 2012 by Neil Bowers <neilb@cpan.org>.
388              
389             This is free software; you can redistribute it and/or modify it under
390             the same terms as the Perl 5 programming language system itself.
391              
392             =cut
393