#!/usr/bin/env perl
use v5.18;

=head1 Abstract

p5find-variable-methods is a program to find method invocations with a
variable name as the method name. Such as:

    $o->$meth()

This is commonly used to reduce repetitions, but it is not grep-friendly, and
could become a source of errors when refactoring. Hopefully this tool can be
helpful on those scenarios.

=head1 Usage Examples

The output format should be compatible to C<grep -Hnr>. Each line consists of
colon-separated fields of filename, line number, and the content of that line.

Here's the result of searching variable method in perlbrew repository:

    > p5find-variable-methods lib/
    lib/App/perlbrew.pm:596:    $self->$s(@args);
    lib/App/perlbrew.pm:1106:    my ($error) = $self->$m_local($dist, $rd);
    lib/App/perlbrew.pm:1107:    ($error) = $self->$m_remote($dist, $rd) if $error;
    lib/App/perlbrew.pm:1127:    $_->mkpath for (grep { ! -d $_ } map { $self->root->$_ } qw(perls dists build etc bin));
    lib/App/perlbrew.pm:1145:            print $fh $self->$method;
    lib/App/perlbrew.pm:1150:                print $fh $self->$method;
    lib/App/perlbrew.pm:2611:        $self->$sub(@args);

=cut

use Getopt::Long qw(GetOptions);
use App::p5find qw(p5_doc_iterator print_file_linenum_line);

sub print_usage {
    print <<USAGE;
p5find-variable-methods [switches] -- [path1] [path2]...

  -h    show help message

For more documentation, see: perldoc p5find-variable-methods
USAGE
}

my %opts;
GetOptions(
    \%opts,
    "h",
);

if ($opts{h}) {
    print_usage();
    exit(0);
}

my @paths = @ARGV;
@paths = ('.') unless @paths;

my $iter = p5_doc_iterator(@paths);
while( my $doc = $iter->() ) {
    my $file = $doc->filename;

    my $arrows = $doc->find(
        sub {
            my $op = $_[1];
            return ($op->isa("PPI::Token::Operator") &&
                    $op->content eq '->')
        }
    ) or next;

    my %hits;
    for (my $i = 0; $i < @$arrows; $i++) {
        my $op = $arrows->[$i];
        my $op_next = $op->snext_sibling;
        next if $op_next->isa("PPI::Token::Word") || $op_next->isa("PPI::Structure::Subscript") || $op_next->isa("PPI::Structure::List");

        # Weird case from PPI. Consider this code:
        #     $a = $b ? $o->foo : 1;
        # The "foo :" part is parsed as one token. Which is wrong.
        # Luckly it does not remove positive responoses if we exclude those here.
        next if $op_next->isa("PPI::Token::Label");

        my $ln = $op->line_number;
        $hits{$ln} = 1;
    }

    if (%hits) {
        print_file_linenum_line( $file, \%hits );
    }
};
