Dennis Williamson
Dennis Williamson

Reputation: 360015

Is there a tool to check a Perl script for unnecessary use statements?

For Python, there is a script called importchecker which tells you if you have unnecessary import statements.

Is there a similar utility for Perl use (and require) statements?

Upvotes: 22

Views: 1497

Answers (3)

zdim
zdim

Reputation: 66883

There is a number of ways to load packages and import symbols (or not). I am not aware of a tool which single-handedly and directly checks whether those symbols are used or not.

But for cases where an explicit import list is given,

use Module qw(func1 func2 ...);

there is a Perl::Critic policy TooMuchCode::ProhibitUnusedImport that helps with much of that.

One runs on the command line

perlcritic --single-policy TooMuchCode::ProhibitUnusedImport program.pl

and the program is checked. Or run without --single-policy flag for a complete check and seek Severity 1 violations in the output, which this is.

For an example, consider a program

use warnings;
use strict;
use feature 'say';

use Path::Tiny;                      # a class; but it imports 'path'    
use Data::Dumper;                    # imports 'Dumper' 
use Data::Dump qw(dd pp);            # imports 'dd' and 'pp'
use Cwd qw(cwd);                     # imports only 'cwd'
use Carp qw(carp verbose);           # imports 'carp'; 'verbose' isn't a symbol
use Term::ANSIColor qw(:constants);  # imports a lot of symbols

sub a_func {
    say "\tSome data: ", pp [ 2..5 ];
    carp "\tA warning";
}

say "Current working directory: ", cwd;

a_func();

Running the above perlcritic command prints

Unused import: dd at line 7, column 5.  A token is imported but not used in the same code.  (Severity: 1)
Unused import: verbose at line 9, column 5.  A token is imported but not used in the same code.  (Severity: 1)

We got dd caught, while pp from the same package isn't flagged since it's used (in the sub), and neither are carp and cwd which are also used; as it should be, out of what the policy aims for.

But note

  • whatever comes with :constants tag isn't found

  • word verbose, which isn't a function (and is used implicitly), is reported as unused

  • if a_func() isn't called then those pp and carp in it are still not reported even though they are then unused. This may be OK, since they are present in code, but it is worth noting

(This glitch-list is likely not exhaustive.)

Recall that the import list is passed to an import sub, which may expect and make use of whatever the module's design deemed worthy; these need not be only function names. It is apparently beyond this policy to follow up on all that. Still, loading modules with the explicit import list with function names is good practice and what this policy does cover is an important use case.

Also, per the clearly stated policy's usage, the Dumper (imported by Data::Dumper) isn't found, nor is path from Path::Tiny. The policy does deal with some curious Moose tricks.

How does one do more? One useful tool is Devel::Symdump, which harvests the symbol tables. It catches all symbols in the above program that have been imported (no Path::Tiny methods can be seen if used, of course). The non-existing "symbol" verbose is included as well though. Add

use Devel::Symdump;

my $syms = Devel::Symdump->new;
say for $syms->functions;

to the above example. To also deal with (runtime) require-ed libraries we have to do this at a place in code after they have been loaded, what can be anywhere in the program. Then best do it in an END block, like

END {
    my $ds = Devel::Symdump->new;
    say for $ds->functions;
};

Then we need to check which of these are unused. At this time the best tool I'm aware of for that job is PPI; see a complete example. Another option is to use a profiler, like Devel::NYTProf.


Another option, which requires some legwork, is the compiler's backend B::Xref, which gets practically everything that is used in the program. It is used as

perl -MO=Xref,-oreport_Xref.txt find_unused.pl

and the (voluminous) output is in the file report_Xref.txt.

The output has sections for each involved file, which have subsections for subroutines and their packages. The last section of the output is directly useful for the present purpose.

For the example program used above I get the output file like

File /.../perl5/lib/perl5//Data/Dump.pm
  ... 
  (some 3,000 lines)
  ...
File find_unused.pl          --> there we go, this program's file
  Subroutine (definitions)
    ... dozens of lines ...  
  Subroutine (main)
    Package main
      &a_func           &43
      &cwd              &27
  Subroutine a_func
    Package ?
      @??               14
    Package main
      &carp             &15
      &pp               &14

So we see that cwd gets called (on line 27) and that carp and pp are also called in the sub a_func. Thus dd and path are unused (out of all imported symbols found otherwise, by Devel::Symdump for example). This is easy to parse.

However, while path is reported when used, if one uses new instead (also in Path::Tiny as a traditional constructor) then that isn't reported in this last section, nor are other methods.

So in principle this is one way to find which of the symbols (for functions) reported to exist by Devel::Symdump have been used in the program.


The example here is simple and easy to process but I have no idea how complete, or hard to parse, this is when all kinds of weird ways for using imported subs are taken into account.

Upvotes: 5

stu42j
stu42j

Reputation: 495

Here is a script I wrote to attempt this. It is very simplistic and will not automate anything for you but it will give you something to start with.

#!/usr/bin/perl

use strict;
use v5.14;

use PPI::Document;
use PPI::Dumper;
use PPI::Find;
use Data::Dumper;

my %import;
my $doc = PPI::Document->new($ARGV[0]);

my $use = $doc->find( sub { $_[1]->isa('PPI::Statement::Include') } );
foreach my $u (@$use) {
    my $node = $u->find_first('PPI::Token::QuoteLike::Words');
    next unless $node;
    $import{$u->module} //= [];
    push $import{$u->module}, $node->literal;
}

my $words = $doc->find( sub { $_[1]->isa('PPI::Token::Word') } );


my @words = map { $_->content } @$words;

my %words;
@words{ @words } = 1;

foreach my $u (keys %import) {
    say $u;
    foreach my $w (@{$import{$u}}) {
        if (exists $words{$w}) {
            say "\t- Found $w";
        }
        else {
            say "\t- Can't find $w";
        }
    }
}

Upvotes: 4

Len Jaffe
Len Jaffe

Reputation: 3484

Take a look at Devel::TraceUse it might give you a chunk of what you're looking for.

Upvotes: 4

Related Questions