File Coverage

blib/lib/App/PrereqGrapher.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


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