From b062a70bd66931ab6e2f0702a9971ac56c014ee8 Mon Sep 17 00:00:00 2001 From: James Raspass Date: Tue, 10 Jan 2017 21:46:54 +0000 Subject: [PATCH] Attempt to find unused imports --- lib/Plint.pm | 54 ++++++++++++++++++++++++++++++++++++++++++----- t/unused-import.t | 25 ++++++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 t/unused-import.t diff --git a/lib/Plint.pm b/lib/Plint.pm index 2897ceb..f62b51c 100644 --- a/lib/Plint.pm +++ b/lib/Plint.pm @@ -89,9 +89,10 @@ sub plint { local ( @ARGV, $/ ) = @_; my ( $tokens, @errors ) = Compiler::Lexer::tokenize( $lexer, scalar <> ); - my @vars = {}; - for ( my $i = 0; my $token = $tokens->[$i]; $i++ ) { + my ( @vars, %imports ) = {}; + + TOKEN: for ( my $i = 0; my $token = $tokens->[$i]; $i++ ) { my $type = $token->{type}; if ( $type == T_Return ) { @@ -105,9 +106,42 @@ sub plint { && $token->{data} eq 'undef' && ( $j == $#$tokens || $tokens->[ ++$j ]{type} != T_Comma ); } + elsif ( $type == T_UseDecl ) { + my $pkg = $tokens->[ ++$i ]{data}; + + # Build the used package name up. + $pkg .= $tokens->[ ++$i ]{data} . $tokens->[ ++$i ]{data} + while $tokens->[ $i + 1 ]{type} == T_NamespaceResolver; + + # Pragmas are often false positives. + next if $pkg =~ /^[a-z]+$/; + + # Skip a few false positives. + next if $pkg eq 'Regexp::Common' + || $pkg eq 'feature' + || $pkg eq 'lib'; + + $type = $tokens->[ $i + 1 ]{type}; + + my @imports + = $type == T_RawString || $type == T_String + ? $tokens->[ $i + 1 ]{data} + : $type == T_RegList + ? split ' ', $tokens->[ $i + 3 ]{data} + : (); + + # Skip the whole import list if any look like non functions. + for (@imports) { + next TOKEN if !/^[a-z_]+$/ || $_ eq 'import'; + } + @imports{@imports} = ("$pkg at line $token->{line}") x @imports; + } + elsif ( $type == T_Key ) { + delete $imports{ $token->{data} }; + } elsif ( $type == T_BuiltinFunc ) { - my $data = $token->{data}; + delete $imports{ my $data = $token->{data} }; push @errors, qq/\$_ should be omitted when calling "$data" at line $token->{line}./ @@ -303,8 +337,18 @@ sub plint { } } - push @errors, qq/"$_" is never read from, declared line $vars[-1]{$_}./ - for sort keys %{ $vars[-1] }; + while ( my ( $import, $pkg ) = each %imports ) { + push @errors, qq/Unused import of "$import" from $pkg./ + } + + while ( my ( $var, $line ) = each %{ $vars[-1] } ) { + push @errors, qq/"$var" is never read from, declared line $line./; + } + + # Sort errors by line number. + @errors = sort { + ( $a =~ /(\d+)\.$/ )[0] <=> ( $b =~ /(\d+)\.$/ )[0] || $a cmp $b + } @errors; \@errors, $tokens->[-1]{line}; } diff --git a/t/unused-import.t b/t/unused-import.t new file mode 100644 index 0000000..72f29fd --- /dev/null +++ b/t/unused-import.t @@ -0,0 +1,25 @@ +use t; + +t q(use Foo 'bar'), 'Unused import of "bar" from Foo at line 1.'; +t q(use Foo 'bar'; bar); + +t q(use Foo "bar"), 'Unused import of "bar" from Foo at line 1.'; +t q(use Foo "bar"; bar); + +t q(use Foo qw/bar/), 'Unused import of "bar" from Foo at line 1.'; +t q(use Foo qw/bar/; bar); + +# Funcs with the same name as builtins are fun. +t q(use Time::HiRes 'time'), + 'Unused import of "time" from Time::HiRes at line 1.'; +t q(use Time::HiRes 'time'; time); + +# False positives. +t q(use lib 'lib'); +t q(use warnings 'all'); + +t q(use Exporter 'import'); +t q(use Getopt::Long qw/:config bundling/); +t q(use Regexp::Common 'number'); + +done;