Finding Unused Subroutines, but with PPI

Can I find all unused Perl subroutines with PPI? Ovid tried it with a quick shell script, which is probably good enough for his purposes, and certainly shorter than my solution.

#!/usr/bin/perl
use v5.14;
use strict;
use warnings;
use PPI;
use Scalar::Util qw(blessed);
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Create the PPI document, and add an isa method that takes a list
sub PPI::Element::isa {
my( $self, @classes ) = @_;
foreach my $class ( @classes ) {
return 1 if $self->UNIVERSAL::isa( $class );
}
return 0;
}
my $Document = PPI::Document->new( 'subs.pl' );
die "Could not create PDOM!" unless blessed $Document;
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Get all of the subroutine definitions
my @subs =
map { $_->name }
@{ $Document->find(
sub {
$_[1]->isa( 'PPI::Statement::Sub' )
}
) };
say "All sub definitions: @subs";
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# find the sub calls that use &
# &foo
# &foo()
# \&foo
my @symbols =
map { $_->content =~ s/\A&//r; }
@{ $Document->find(
sub {
$_[1]->isa( 'PPI::Token::Symbol' ) &&
$_[1]->symbol_type eq '&'
}
) || [] };
say "All symbols: @symbols";
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# find the sub calls that use parens
# foo()
# foo( @args )
my @list =
map { $_->literal }
@{ $Document->find(
sub {
$_[1]->isa( 'PPI::Token::Word' ) &&
$_[1]->snext_sibling->isa( 'PPI::Structure::List' )
}
) || [] };
say "All list: @list";
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# find the sub calls that are barewords
# foo
# foo + bar
# but not
# use vars qw( baz );
# sub quux { ... }
my %reserved = map { $_, $_ } qw( use vars sub my );
my @barewords =
map { $_->literal }
grep {
# Take out the Words that are preceded by 'sub'
# That is, take out the subroutine definitions
# I couldn't get this to work inside the find()
my $previous = $_->previous_sibling;
my $sprevious = $_->sprevious_sibling;
! (
blessed( $previous ) &&
blessed( $sprevious ) &&
$previous->isa( 'PPI::Token::Whitespace' ) &&
$sprevious->isa( 'PPI::Token::Word' ) &&
$sprevious->literal eq 'sub'
)
}
@{ $Document->find(
sub {
$_[1]->isa( 'PPI::Token::Word' )
&&
$_[1]->next_sibling->isa( qw(
PPI::Token::Whitespace
PPI::Token::Structure
PPI::Token::List
PPI::Token::Operator
) )
&&
( ! exists $reserved{ $_[1]->literal } )
}
) || [] };
say "All barewords: @barewords";
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Combined the used subs
my %used = map { $_ => 1 } ( @symbols, @list, @barewords );
say "All used: @{ [keys %used] }";
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# The unused have to be the left over ones
my @unused = grep { ! exists $used{$_} } @subs;
say "All unused: @unused";

I don't know if I've covered all of the cases, but it seems to mostly work.

8 Comments

Thanks for this. I started a messy project for general discovery and graphing for legacy CGI projects which is here: http://sourceforge.net/projects/codewalker/

Inspired by this and PPI in general, I'm thinking about taking another run at it. Thanks again, Hugh Barnard

There's always perlcritic --single UnusedPrivateSubroutines, if its assumptions are what you want.

If not, this policy might be a good starting point to roll your own. The policy uses a Perl::Critic::Document rather than a PPI::Document, but the former is easily manufactured from the latter.

(Sorry I'm not good at English)

Thank you for your good post, always.

I tested the above code and it seems that the code fails to catch sub calls in the form of "foo()".

And, if target code has no sub call form "&...", this script dies with:

Can't use string ("") as an ARRAY ref while "strict refs" in use at D:\Temp\find_unused.pl line 42.

Thank you, Brian! This will be a very useful addition to my toolbox.

I tried to run your latest revision and it appears that it still does not recognize any call that uses parentheses, e.g. fubar() ?

Dumping the output of


$_[1]->snext_sibling->isa( 'PPI::Structure::List'
gives me an undef which possibly why @list will always be empty?

Thanks!

Nice, It would be nice to see it as part of PPIx::EditorTools.

Would you do it? If not, do you mind if I add it?

I just noticed this. It's a lovely bit of code. You mention that it's longer than mine, but it has a couple of strong advantages: it handles comments correctly and can easily be extended to other uses.

Leave a comment

Sign in to comment.

About brian d foy

user-pic I'm the author of Mastering Perl, and the co-author of Learning Perl (6th Edition), Intermediate Perl, Programming Perl (4th Edition) and Effective Perl Programming (2nd Edition).