views:

471

answers:

3

I am working on a moderately complex Perl program. As a part of its development, it has to go through modifications and testing. Due to certain environment constraints, running this program frequently is not an option that is easy to exercise.

What I want is a static call-graph generator for Perl. It doesn't have to cover every edge case(e,g., redefining variables to be functions or vice versa in an eval).

(Yes, I know there is a run-time call-graph generating facility with Devel::DprofPP, but run-time is not guaranteed to call every function. I need to be able to look at each function.)

+3  A: 

I don't there is a "static" call-graph generator for Perl.

The next closest thing would be Devel::NYTProf.

The main goal is for profiling, but it's output can tell you how many times a subroutine has been called, and from where.

If you need to make sure every subroutine gets called, you could also use Devel::Cover, which checks to make sure your test-suite covers every subroutine.

Brad Gilbert
http://www.slideshare.net/Tim.Bunce/develnytprof-200907
Brad Gilbert
Brad, the problem is that run-time profiling in the general case will not give you every function in the program. There will only be a limited subset of the program run in each execution. That's why I specifically want a static analyzer.
Paul Nathan
That's why I also mentioned `Devel::Cover`, it makes sure all of your subroutines get called.
Brad Gilbert
+6  A: 

Can't be done in the general case:

my $obj    = Obj->new;
my $method = some_external_source();

$obj->$method();

However, it should be fairly easy to get a large number of the cases (run this program against itself):

#!/usr/bin/perl

use strict;
use warnings;

sub foo {
    bar();
    baz(quux());
}

sub bar {
    baz();
}

sub baz {
    print "foo\n";
}

sub quux {
    return 5;
}

my %calls;

while (<>) {
    next unless my ($name) = /^sub (\S+)/;
    while (<>) {
        last if /^}/;
        next unless my @funcs = /(\w+)\(/g;
        push @{$calls{$name}}, @funcs;
    }
}

use Data::Dumper;
print Dumper \%calls;

Note, this misses

  • calls to functions that don't use parentheses (e.g. print "foo\n";)
  • calls to functions that are dereferenced (e.g. $coderef->())
  • calls to methods that are strings (e.g. $obj->$method())
  • calls the putt the open parenthesis on a different line
  • other things I haven't thought of

It incorrectly catches

  • commented functions (e.g. #foo())
  • some strings (e.g. "foo()")
  • other things I haven't thought of

If you want a better solution than that worthless hack, it is time to start looking into PPI, but even it will have problems with things like $obj->$method().

Just because I was bored, here is a version that uses PPI. It only finds function calls (not method calls). It also makes no attempt to keep the names of the subroutines unique (i.e. if you call the same subroutine more than once it will show up more than once).

#!/usr/bin/perl

use strict;
use warnings;

use PPI;
use Data::Dumper;
use Scalar::Util qw/blessed/;

sub is {
    my ($obj, $class) = @_;
    return blessed $obj and $obj->isa($class);
}

my $program = PPI::Document->new(shift);

my $subs = $program->find(
    sub { $_[1]->isa('PPI::Statement::Sub') and $_[1]->name }
);

die "no subroutines declared?" unless $subs;

for my $sub (@$subs) {
    print $sub->name, "\n";
    next unless my $function_calls = $sub->find(
        sub { 
            $_[1]->isa('PPI::Statement')             and
            $_[1]->child(0)->isa("PPI::Token::Word") and
            not (
                $_[1]->isa("PPI::Statement::Scheduled") or
                $_[1]->isa("PPI::Statement::Package")   or
                $_[1]->isa("PPI::Statement::Include")   or
                $_[1]->isa("PPI::Statement::Sub")       or
                $_[1]->isa("PPI::Statement::Variable")  or
                $_[1]->isa("PPI::Statement::Compound")  or
                $_[1]->isa("PPI::Statement::Break")     or
                $_[1]->isa("PPI::Statement::Given")     or
                $_[1]->isa("PPI::Statement::When")
            )
        }
    );
    print map { "\t" . $_->child(0)->content . "\n" } @$function_calls;
}
Chas. Owens
Chas - I'd be curious to see your code, but just as FYI, some attempts already exist, e.g. http://lists.netisland.net/archives/phlpm/phlpm-2004/msg00024.html
DVK
+4  A: 

I'm not sure it is 100% feasible (since Perl code can not be statically analyzed in theory, due to BEGIN blocks and such - see very recent SO discussion). In addition, subroutine references may make it very difficult to do even in places where BEGIN blocks don't come into play.

However, someone apparently made the attempt - I only know of it but never used it so buyer beware.

DVK
It's not 100% workable. I'm OK with that. 90% is sufficient in my opinion.
Paul Nathan