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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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.
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 aPPI::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.
Thanks, I've fixed the two problems that you've noted. The death came from my use of
@{ ... }
. If the thing inside is not an array ref, as in the case offind
returning no elements, the dereference fails. I just need a default, so I have@{ ... || [] }
I left out the bits to find calls in the form of
foo()
. I've re-added that.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
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?
My answer for all new projects for the rest of this year is "No", so I guess that means you get to do it. It is open source, after all. :)
And, you get to fix the bugs too. :)
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.