18

I want to scan a code base to identify all instances of undefined subroutines that are not presently reachable.

As an example:

use strict;
use warnings;

my $flag = 0;
if ( $flag ) {
  undefined_sub();
}

Observations

  • When $flag evaluates to true, the following warning is emitted:

    Undefined subroutine &main::undefined_sub called at - line 6
    

    I don't want to rely on warnings issued at run-time to identify undefined subroutines

  • The strict and warnings pragmas don't help here. use strict 'subs' has no effect.

  • Even the following code snippet is silent

    $ perl -Mstrict -we 'exit 0; undefined_sub()'
    
Zaid
  • 36,680
  • 16
  • 86
  • 155
  • 6
    Very interesting question! There's a related [thread at perlmonks](http://www.perlmonks.org/?node_id=391783). – PerlDuck Jan 02 '17 at 14:06
  • @PerlDuck one of the suggestions in that thread suggests using [`B::Lint::undefined_subs`](https://metacpan.org/pod/B::Lint) but even that appears to rely on the subroutine being invoked. – Zaid Jan 02 '17 at 14:17
  • Up to Perl 5.18 `B::Lint` was a core module. I have 5.22 and just installed it [from CPAN](http://search.cpan.org/~rjbs/B-Lint-1.20/lib/B/Lint.pm). `perl -MO=Lint so.pl` correctly says `Nonexistent subroutine 'undefined_sub' called at so.pl line 7`. But I've no idea how it does that. – PerlDuck Jan 02 '17 at 14:23
  • Probably: After compiling the program, it checks the subs called by the program against the existing subs. That should be rather reliable, especially since it results in false positives rather than false negatives. – ikegami Jan 02 '17 at 14:53
  • ...Unfortunately, B::Lint only checks the top-level code. That's a limitation of the module, not of the technique. – ikegami Jan 02 '17 at 15:21
  • sub maybe defined dynamically: `eval "sub undefined_sub {}"`. And there is **no way** to check will it be defined or will not – Eugen Konkov Jul 05 '17 at 11:10

3 Answers3

13

Perhaps Subroutines::ProhibitCallsToUndeclaredSubs policy from Perl::Critic can help

This Policy checks that every unqualified subroutine call has a matching subroutine declaration in the current file, or that it explicitly appears in the import list for one of the included modules.

This "policy" is a part of Perl::Critic::StricterSubs, which needs to be installed. There are a few more policies there. This is considered a severity 4 violation, so you can do

perlcritic -4 script.pl

and parse the output for neither declared nor explicitly imported, or use

perlcritic -4 --single-policy ProhibitCallsToUndeclaredSubs script.pl

Some legitimate uses are still flagged, since it requires all subs to be imported explicitly.

This is a static analyzer, which I think should fit your purpose.

zdim
  • 64,580
  • 5
  • 52
  • 81
  • This is a great find... I was thinking that `PPI` may be the way forward here but this solution takes it to the next level, and definitely fits my needs. – Zaid Jan 03 '17 at 05:14
  • @Zaid Great :)) Also, `perlcritic` can be configured, and policies can be tweaked or written. I also ran into [Sub::StrictDecl](http://search.cpan.org/dist/Sub-StrictDecl-0.003/lib/Sub/StrictDecl.pm) but don't know anything about it. Anyway, one can do a lot more with `perlcritic`. – zdim Jan 03 '17 at 06:42
1

What you're asking for is in at least some sense impossible. Consider the following code snippet:

( rand()<0.5 ? *foo : *bar } = sub { say "Hello World!" };

foo();

There is a 50% chance that this will run OK, and a 50% chance that it will give an "Undefined subroutine" error. The decision is made at runtime, so it's not possible to tell before that what it will be. This is of course a contrived case to demonstrate a point, but runtime (or compile-time) generation of subroutines is not that uncommon in real code. For an example, look at how Moose adds functions that create methods. Static source code analysis will never be able to fully analyze such code.

B::Lint is probably about as good as something pre-runtime can get.

ikegami
  • 367,544
  • 15
  • 269
  • 518
Calle Dybedahl
  • 5,228
  • 2
  • 18
  • 22
  • 3
    Detecting calls to undefined subs at compile-time is actually quite reliable. 1) Compile-time generation of subs or methods is not a problem. These would be handled correctly by B::Lint. 2) Runtime generation of methods really is quite rare. Your Moose example doesn't fly because the methods will be generated when the module is loaded during it's user's compile-time. 3) Detecting calls to undefined methods is next to impossible, but not for the reasons you gave. 4) The OP didn't ask about calls to undefined method. 5) Runtime generation of subs is even rarer. – ikegami Jan 02 '17 at 15:04
1

To find calls to subs that aren't defined at compile time, you can use B::Lint as follows:

a.pl:

use List::Util qw( min );

sub defined_sub { }
sub defined_later;
sub undefined_sub;

defined_sub();
defined_later();
undefined_sub();
undeclared_sub();
min();
max();              # XXX Didn't import
List::Util::max();
List::Util::mac();  # XXX Typo!

sub defined_later { }

Test:

$ perl -MO=Lint,undefined-subs a.pl
Undefined subroutine 'undefined_sub' called at a.pl line 9
Nonexistent subroutine 'undeclared_sub' called at a.pl line 10
Nonexistent subroutine 'max' called at a.pl line 12
Nonexistent subroutine 'List::Util::mac' called at a.pl line 14
a.pl syntax OK

Note that this is just for sub calls. Method calls (such as Class->method and method Class) aren't checked. But you are asking about sub calls.

Note that foo $x is a valid method call (using the indirect method call syntax) meaning $x->foo if foo isn't a valid function or sub, so B::Lint won't catch that. But it will catch foo($x).

ikegami
  • 367,544
  • 15
  • 269
  • 518