#!/usr/bin/perl -w
use strict;

# Walk through a perl script and look for 'naughty match variables'
# $`, $&, and $', which may cause poor performance.
#
# usage:
# find_naughty file1 [file2 [...]]
# find_naughty <file.pl
#
# Author: Steve Hancock, July 2003
#
# TODO:
# - recursive processing might be nice
#
# Inspired by the discussion of naughty match variables at:
# http://www.perlmonks.org/index.pl?node_id=276549
#
use Getopt::Std;
use IO::File;
$| = 1;
use vars qw($opt_h);
main();

sub main {
    my $usage = <<EOM;
usage:
  find_naughty file1 [file2 [...]]
  find_naughty <file.pl 
EOM
    getopts('h') or die "$usage";
    if ($opt_h) { die $usage }

    unless (@ARGV) { unshift @ARGV, '-' }    # stdin
    foreach my $source (@ARGV) {
        PerlTokenSearch::find_naughty( _source => $source, );
    }
    return;
}

#####################################################################
#
# The PerlTokenSearch package is an interface to perltidy which accepts a
# source filehandle and looks for selected variables.
#
# It works by making a callback object with a write_line() method to
# receive tokenized lines from perltidy.
#
# Usage:
#
#   PerlTokenSearch::find_naughty(
#       _source         => $fh,             # required source
#   );
#
# _source is any source that perltidy will accept, including a
# filehandle or reference to SCALAR or ARRAY
#
#####################################################################

package PerlTokenSearch;
use Carp;
use Perl::Tidy;

sub find_naughty {

    my %args = (@_);
    print "Testing File: $args{_source}\n";

    # run perltidy, which will call $formatter's write_line() for each line
    my $err = perltidy(
        'source'    => $args{_source},
        'formatter' => bless( \%args, __PACKAGE__ ),    # callback object
        'argv'      => "-npro -se",    # -npro : ignore .perltidyrc,
                                       # -se   : errors to STDOUT
    );
    if ($err) {
        die "Error calling perltidy\n";
    }
}

sub write_line {

    # This is called back from perltidy line-by-line
    # We're looking for $`, $&, and $'
    my ( $self, $line_of_tokens ) = @_;
    my $source = $self->{_source};

    # pull out some stuff we might need
    my $line_type         = $line_of_tokens->{_line_type};
    my $input_line_number = $line_of_tokens->{_line_number};
    my $input_line        = $line_of_tokens->{_line_text};
    my $rtoken_type       = $line_of_tokens->{_rtoken_type};
    my $rtokens           = $line_of_tokens->{_rtokens};
    chomp $input_line;

    # skip comments, pod, etc
    return if ( $line_type ne 'CODE' );

    # loop over tokens looking for $`, $&, and $'
    for ( my $j = 0 ; $j < @$rtoken_type ; $j++ ) {

        # we only want to examine token types 'i' (identifier)
        next unless $$rtoken_type[$j] eq 'i';

        # pull out the actual token text
        my $token = $$rtokens[$j];

        # and check it
        if ( $token =~ /^\$[\`\&\']$/ ) {
            print STDERR "$source:$input_line_number: $token\n";
        }
    }
}

# optional routine, called once after the last line of a file
sub finish_formatting {
    my $self = shift;
    return;
}
