File Coverage

blib/lib/App/PrereqGrapher.pm
Criterion Covered Total %
statement 65 102 63.7
branch 17 50 34.0
condition 8 18 44.4
subroutine 11 15 73.3
pod 1 5 20.0
total 102 190 53.6


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