如何为 Perl 制作静态分析调用图?

发布于 2024-08-01 18:12:55 字数 247 浏览 13 评论 0原文

我正在开发一个中等复杂的 Perl 程序。 作为其开发的一部分,它必须经过修改和测试。 由于某些环境限制,频繁运行该程序并不是一个容易练习的选择。

我想要的是 Perl 的静态调用图生成器。 它不必涵盖所有边缘情况(例如,在 eval 中将变量重新定义为函数,反之亦然)。

(是的,我知道 Devel::DprofPP 有一个运行时调用图生成工具,但运行时并不能保证调用每个函数。我需要能够查看每个函数功能。)

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.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

琉璃繁缕 2024-08-08 18:12:55

在一般情况下无法完成:

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

$obj->$method();

但是,获得大量情况应该相当容易(针对自身运行此程序):

#!/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;

注意,这会错过

  • 对不使用括号的函数的调用(例如 print "foo\n";)
  • 调用取消引用的函数(例如 $coderef->())
  • 调用字符串方法(例如 $obj- >$method())
  • 在不同的行上调用左括号
  • 其他我没有想到的事情

它错误地捕获了

  • 注释函数(例如#foo()
  • 一些字符串(例如“foo()”
  • 我没有想到的其他事情

如果您想要一个比毫无价值的黑客更好的解决方案,那么是时候开始研究PPI,但即使它也会遇到诸如 $obj 之类的问题->$method()

只是因为我很无聊,这里有一个使用 PPI。 它只查找函数调用(而不是方法调用)。 它也不尝试保持子例程名称的唯一性(即,如果您多次调用同一个子例程,它将多次显示)。

#!/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;
}

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;
}
£烟消云散 2024-08-08 18:12:55

我不确定它是否 100% 可行(因为 Perl 代码理论上无法静态分析,由于 BEGIN 块等 - 请参阅 最近的 SO 讨论)。 此外,即使在 BEGIN 块不发挥作用的地方,子例程引用也可能使其变得非常困难。

然而,显然有人做出了尝试 - 我只知道但从未使用过,所以买家要小心。

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.

对不⑦ 2024-08-08 18:12:55

我最近在尝试解决同一问题的答案时偶然发现了一个脚本。 该脚本(链接到下面)使用 GraphViz 创建 Perl 程序或模块的调用图。 输出可以是多种图像格式。

http://www.teragridforum.org/mediawiki/index.php?title=Perl_Static_Source_Code_Analysis

I recently stumbled across a script while trying to solve find an answer to this same question. The script (linked to below) uses GraphViz to create a call graph of a Perl program or module. The output can be in a number of image formats.

http://www.teragridforum.org/mediawiki/index.php?title=Perl_Static_Source_Code_Analysis

另类 2024-08-08 18:12:55

我认为 Perl 没有“静态”调用图生成器。

下一个最接近的东西是 Devel::NYTProf

主要目标是进行分析,但它的输出可以告诉您子例程被调用的次数以及从何处调用。

如果您需要确保调用每个子例程,您还可以使用 Devel::Cover,它会检查以确保您的测试套件涵盖每个子例程。

I don't think 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.

太阳男子 2024-08-08 18:12:55

我最近解决了类似的问题,想分享我的解决方案。
这个工具是在绝望中诞生的,它解开了 30,000 行遗留脚本中未记录的部分,以实现紧急的错误修复。

它读取源代码,使用 GraphViz 生成 png,然后在屏幕上显示图像。
由于它使用简单的逐行正则表达式,因此格式必须“合理”,以便可以确定嵌套。
如果目标代码格式错误,请先通过 linter 运行它。
另外,不要指望诸如解析动态函数调用之类的奇迹。

简单的正则表达式引擎的一线希望是它可以轻松扩展到其他语言。
该工具现在还支持 awk、bash、basic、dart、fortran、go、lua、javascript、kotlin、matlab、pascal、perl、php、python、r、raku、ruby、rust、scala、swift 和 tcl。

https://github.com/koknat/callGraph

I solved a similar problem recently, and would like to share my solution.
This tool was born out of desperation, untangling an undocumented part of a 30,000-line legacy script, in order to implement an urgent bug fix.

It reads the source code(s), uses GraphViz to generate a png, and then displays the image on-screen.
Since it uses simple line-by-line regexes, the formatting must be "sane" so that nesting can be determined.
If the target code is badly formatted, run it through a linter first.
Also, don't expect miracles such as parsing dynamic function calls.

The silver lining of a simple regex engine is that it can be easily extended for other languages.
The tool now also supports awk, bash, basic, dart, fortran, go, lua, javascript, kotlin, matlab, pascal, perl, php, python, r, raku, ruby, rust, scala, swift, and tcl.

https://github.com/koknat/callGraph

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文