The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package App::PrereqGrapher;
$App::PrereqGrapher::VERSION = '0.12';
#
# ABSTRACT: generate dependency graph using Perl::PrereqScanner
#

use 5.006;
use strict;
use warnings;

use Carp;
use Moo;
use Perl::PrereqScanner;
use Getopt::Long qw/:config no_ignore_case/;
use Graph::Easy;
use Module::Path qw(module_path);
use Module::CoreList;

my %formats =
(
    'dot'  => sub { $_[0]->as_graphviz; },
    'svg'  => sub { $_[0]->as_svg; },
    'gml'  => sub { $_[0]->as_graphml; },
    'vcg'  => sub { $_[0]->as_vcg; },
    'html' => sub { $_[0]->as_html_file; },
);

has format => (
    is      => 'ro',
    isa     => sub { croak "valid formats: ", join(", ", keys %formats), "\n"  unless exists $formats{$_[0]}; },
    default => sub { return 'dot'; },
);

has output_file => (
    is  => 'ro',
);

has no_core => (
    is => 'ro',
);

has no_recurse_core => (
    is => 'ro',
);

has verbose => (
    is => 'ro',
);

has depth => (
    is => 'ro',
    isa => sub { croak "depth must be an integer\n" unless $_[0] =~ /^\d+$/; },
);

has timeout => (
    is => 'ro',
    isa => sub { croak "timeout must be an integer\n" unless $_[0] =~ /^\d+$/; },
);

sub new_with_options
{
    my $class    = shift;
    my %options  = $class->parse_options();
    my $instance = $class->new(%options, @_);

    return $instance;
}

sub parse_options
{
    my $class = shift;
    my %options;
    my %format;

    GetOptions(
        'h|help'              => \$options{'help'},
        'd|depth=i'           => \$options{'depth'},
        't|timeout=i'         => \$options{'timeout'},
        'o|output-file=s'     => \$options{'output_file'},
        'nc|no-core'          => \$options{'no_core'},
        'nrc|no-recurse-core' => \$options{'no_recurse_core'},
        'v|verbose'           => \$options{'verbose'},
        'dot'                 => \$format{'dot'},
        'svg'                 => \$format{'svg'},
        'gml'                 => \$format{'gml'},
        'vcg'                 => \$format{'vcg'},
        'html'                => \$format{'html'},
    ) || croak "Can't get options.";
    usage() if $options{'help'};

    foreach my $format (keys %formats) {
        delete $format{$format} unless $format{$format};
    }
    if (keys %format > 1) {
        print "FORMAT: ", join(', ', keys %format), "\n";
        croak "you can only specify at most ONE output format (default is 'dot')";
    }
    $format{dot} = 1 unless keys %format == 1;

    if ($options{no_core} && $options{no_recurse_core}) {
        croak "doesn't make sense to specify no-core and no-recurse-core together";
    }

    for (keys %options) {
        delete $options{$_} unless defined $options{$_};
    }
    $options{format} = (keys %format)[0];

    return %options;
}

sub generate_graph
{
    my ($self, @inputs) = @_;
    my (@queue, %seen, $scanner, $graph, $module);
    my ($prereqs, $depsref);
    my ($path, $filename, $fh);
    my $module_count = 0;
    my %depth;

    $scanner = Perl::PrereqScanner->new;
    $graph   = Graph::Easy->new();
    $graph->timeout($self->timeout);

    @depth{@inputs} = map { 0 } @inputs;

    push(@queue, @inputs);
    while (@queue > 0) {
        $module = shift @queue;
        next if $seen{$module};
        $seen{$module} = 1;

        if (defined($path = module_path($module))) {
        } elsif (-f $module) {
            $path = $module;
        } else {
            carp "can't find $module - keeping calm and carrying on.\n" if $self->verbose;
            next;
        }

        # Huge files (eg currently Perl::Tidy) will cause PPI to barf
        # So we need to catch those, keep calm, and carry on
        eval { $prereqs = $scanner->scan_file($path); };
        next if $@;
        ++$module_count;
        $depsref = $prereqs->as_string_hash();
        foreach my $dep (keys %{ $depsref }) {
            if (!exists($depth{$dep}) || $depth{$dep} > $depth{$module} + 1) {
                $depth{$dep} = $depth{$module} + 1;
            }
            if ($self->no_core && is_core($dep)) {
                # don't include core modules
            } elsif ($dep eq 'perl') {
                $graph->add_edge($module, "perl $depsref->{perl}");
            } else {
                $graph->add_edge($module, $dep);
                next if $self->depth && $depth{$dep} >= $self->depth;
                push(@queue, $dep) unless $self->no_recurse_core && is_core($dep);
            }
        }
    }

    $filename = $self->output_file || 'dependencies.'.$self->format;
    open($fh, '>', $filename) ||
        croak "Failed to write $filename: $!\n";
    print $fh $formats{$self->format}->($graph);
    close($fh);
    print STDERR "$module_count modules processed. Graph written to $filename\n" if $self->verbose;
}

sub is_core
{
    my $module   = shift;
    my $version  = @_ > 0 ? shift : $^V;

    return 0 unless defined(my $first_release = Module::CoreList::first_release($module));
    return 0 unless $version >= $first_release;
    return 1 if !defined(my $final_release = Module::CoreList::removed_from($module));
    return $version <= $final_release;
}

sub usage
{
    require Pod::Usage;
    Pod::Usage::pod2usage();
    exit;
}

1;

=head1 NAME

App::PrereqGrapher - generate dependency graph using Perl::PrereqScanner

=head1 SYNOPSIS

  use App::PrereqGrapher;
  
  my %options = (
                          format => 'dot',
                         no_core => 0,
                 no_recurse_core => 1,
                     output_file => 'prereqs.dot',
                         verbose => 0,
                );
  my $grapher = App::PrereqGrapher->new( %options );
  
  $grapher->generate_graph('Module::Path');

=head1 DESCRIPTION

App::PrereqGrapher builds a directed graph of the prereqs or dependencies for
a file or module. It uses Perl::PrereqScanner to find the dependencies for the seed,
and then repeatedly calls Perl::PrereqScanner on those dependencies, and so on,
until all dependencies have been found.

It then saves the resulting graph to a file, using one of the five supported formats.
The default format is 'dot', the format used by the GraphViz graph drawing toolkit.

If your code contains lines like:

 require 5.006;
 use 5.006;

Then you'll end up with a dependency labelled B<perl 5.006>;
this way you can see where you're dependent on modules which
require different minimum versions of perl.

=head1 METHODS

=head2 new

The constructor understands the following options:

=over 4

=item format

Select the output format, which must be one of: dot, svg, vcg, gml, or html.
See L</"OUTPUT FORMATS"> for more about the supported output formats.
If not specified, the default format is 'dot'.

=item output_file

Specifies the name of the file to write the dependency graph into,
including the extension. If not specified, the filename will be C<dependencies>,
with the extension set according to the format.

=item depth

Only generate the graph to the specified depth.
If the complete dependency graph is very large, this option may help you get
an overview.

=item no_core

Don't include any modules which are core (included with perl) for the version
of perl being used.

=item no_recurse_core

When a core module is used, include it in the dependency graph,
but don't show any of I<its> dependencies.

=item timeout

Specifies the timeout for Graph::Easy when its laying out the graph.
This is mainly relevant for formats like SVG and HTML, where the graph
layout is done in Perl. Defaults to 5 seconds.

=item verbose

Display verbose logging as the grapher runs.
Currently this will just tell you if a module was use'd or require'd,
but couldn't be found locally.

=back

=head2 generate_graph

Takes one or more seed items. Each item may be a module or the path to a perl file.

  $grapher->generate_graph('Module::Path', 'Module::Version');

It will first try and interpret each item as a module, but if it can't find a module
with the given name, it will try and interpret it as a file path.
This means that if you have a file called C<strict> for example, then you won't be
able to run:

  $grapher->generate_graph('strict');

as it will be interpreted as the module of that name. Put an explicit path to stop this:

  $grapher->generate_graph('./strict');

=head1 OUTPUT FORMATS

=over 4

=item dot

The format used by GraphViz and related tools.

=item svg

Scalable Vector Graphics (SVG) a W3C standard.
You have to install L<Graph::Easy::As_svg> if you want to use this format.

=item vcg

The VCG or GDL format.

=item gml

Graph Markup Language, aka GraphML.

=item html

Generate an HTML format with embedded CSS. I haven't been able to get this to work,
but it's one of the formats supported by L<Graph::Easy>.

=back

=head1 KNOWN BUGS

L<Perl::PrereqScanner> uses L<PPI> to parse each item.
PPI has a hard-coded limit for the size of file it's prepared to parse
(currently just over 1M).
This means that very large files will be ignored;
for example Perl::Tidy cannot be graphed,
and if you try and graph a file that use's Perl::Tidy,
then it just won't appear in the graph.

If a class isn't defined in its own file,
then App::PrereqGrapher won't find it;
for example Tie::StdHash is defined inside Tie::Hash.
By default these are silently ignored,
but if you use the B<-verbose> option you'll get the following warning:

  can't find Tie::StdHash - keeping calm and carrying on.

Perl::PrereqScanner parses code and makes no attempt to
determine whether any of it would actually run on your platform.
For example, one module might decide at run-time whether to C<require>
Foo::Bar or Foo::Baz, and might never use Foo::Baz on your OS.
But Perl::PrereqScanner will see both of Foo::Bar and Foo::Baz
as pre-reqs.

=head1 TODO

=over 4

=item *

Have an option to control what depth we should recurse to?
You might only be interested in the dependencies of your code,
and their first level of dependencies.

=item *

Show some indication that we're running. It can take a long time to run
if your ultimate dependency graph is very large.

=back

=head1 SEE ALSO

The distribution for this module contains a command-line script,
L<prereq-grapher>. It has its own documentation.

This module uses L<Perl::PrereqScanner> to parse the source code,
and L<Graph::Easy> to generate and save the dependency graph.

L<http://neilb.org/reviews/dependencies.html>: a review of CPAN modules that can be used
to get dependency information.

=head1 REPOSITORY

L<https://github.com/neilb/App-PrereqGrapher>

=head1 AUTHOR

Neil Bowers E<lt>neilb@cpan.orgE<gt>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2012 by Neil Bowers <neilb@cpan.org>.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut