How can I find any methods that contain a specific string in a Perl code set?

advertisements

I've been given a tricky task which is to write a log entry at certain points within a large million+ line code base.

The points that I need to log can be found from a list of 500+ template types. A template type is simply a string like "end_assignment_affiliate" or "interview_accepted".

I'm trying to work out how to write a perl script that will take the list of 500 templates and then search through the code to find the methods that make use of each specific template string (I then hope to use the list of methods to find all the entry points into the system for each template type).

So for example I may have

sub aSub {
 my($arg) = @_
 ...
 if ($template eq 'interview_accepted') {
 ...
}

I want to determine that method aSub contains interview_accepted. interview_accepted may be contained within multiple subroutines.

It's quite easy to grep the code base for the message type and find the line number in files where that message exists, however I'm having a hard time trying to identify the containing method.

Clearly if I can do this programmatically it will be more robust, repeatable and much quicker.

Does anyone know of any modules or tricks that I can use to achieve this?

Update

I'm currently playing with using File::ReadBackwards to find the string, and then from that point find the first sub [name] { point. I'm wondering though if there is a more elegant solution?


My CPAN module Devel::Examine::Subs can do this with the has() method or function. Here's a sample script using the OO version, which will do what you want. Just enter the directory you want to search (recursively) as argument one, and the search term as argument two:

#!/usr/bin/perl
use warnings;
use strict;
use 5.18.0;

use Devel::Examine::Subs;
use File::Find;

my $des = Devel::Examine::Subs->new();

my $dir = $ARGV[0];
my $search = $ARGV[1];

find({ wanted => \&check_subs, no_chdir => 1 },
       $dir,
);

sub check_subs {

    if (! -f or ! /(?:\.pm|\.pl)$/){
        return;
    } 

    my $file = "$File::Find::name";

    my @has = $des->has({file => $file, search => $search});

    return if ! @has;

    say "\n$file:" ;
    say "\t$_" for @has;
}

Called like this perl des.pl business-isp/ template results in this example output:

business-isp/lib/Business/ISP/Reports.pm:
    income_by_item
    renewal_notices

business-isp/lib/Business/ISP/GUI/Accounting.pm:
    _display_plan_stats
    process_renew
    display_add_plan
    email_invoice
    process_purchase
    display_payment_form
    client_delete
    _contact_info_table
    show_plan
    display_uledger
    add_plan

business-isp/lib/Business/ISP/GUI/Base.pm:
    start
    _header
    display_config
    _render_error
    _blank_header
    _footer

UPDATE: I've modified the script slightly so it can be used in a loop with a bunch of search terms. Just populate your template names into the @searches array, and specify the directory structure to search in $dir.

#!/usr/bin/perl
use warnings;
use strict;
use 5.18.0;

use Devel::Examine::Subs;
use File::Find;

my $des = Devel::Examine::Subs->new();

my $dir = 'business-isp/';
my @searches = qw(template this that other);

for my $search (@searches){

    say "\n***** SEARCHING FOR: $search *****\n";

    find({ wanted => sub { check_subs($search) }, no_chdir => 1 },
           $dir
    );
}

sub check_subs {

    my $search = shift;

    if (! -f or ! /(?:\.pm|\.pl)$/){
        return;
    } 

    my $file = "$File::Find::name";

    my @has = $des->has({file => $file, search => $search});

    return if ! @has;

    say "\n$file:" ;
    say "\t$_" for @has;
}

UPDATE: Here's a script that uses the new has() method with the lines parameter set. It retrieves the entire line that the search hits, along with the line number it's on:

#!/usr/bin/perl
use warnings;
use strict;
use 5.18.0;

use Devel::Examine::Subs;
use File::Find;

my $des = Devel::Examine::Subs->new();

my $dir = 'business-isp/';
my @searches = qw(date);

for my $search (@searches){

    say "\n***** SEARCHING FOR: $search *****\n";

    find({ wanted => sub { check_subs($search) }, no_chdir => 1 },
           $dir
    );
}

sub check_subs {

    my $search = shift;

    if (! -f or ! /(?:\.pm|\.pl)$/){
        return;
    } 

    my $file = "$File::Find::name";

    my %subs = $des->has({file => $file, search => $search, lines => 1});

    return if not %subs;

    print "\n$file:\n\n";

    for my $sub (keys %subs){    

        print "$sub:\n";

        for my $line_info (@{$subs{$sub}}){
            while (my ($k, $v) = each (%$line_info)){
                print "\tLine num: $k, Line Data: $v\n";
            }
        }
    }
}

Output:

business-isp/lib/Business/ISP/Sanity.pm:

validate_data:
    Line num: 168, Line Data:  $self->validate_value({
audit:
    Line num: 72, Line Data:  my $date = $self->date({ get => $schedule });
    Line num: 77, Line Data:  # update the audit list if the process is claiming to be
    Line num: 86, Line Data:  date => $self->date({ get => 'day' }),
    Line num: 108, Line Data:  date => { -like => "$date%" },
    Line num: 123, Line Data:  my $executed_date = $executed->date;
    Line num: 126, Line Data:  "Process $process has already run its $schedule cycle on $executed_date";
validate_renew:
    Line num: 304, Line Data:  $self->validate_value({
validate_value:
    Line num: 193, Line Data:  # return if validate_value is disabled!
    Line num: 204, Line Data:  print "Sanity validate_value_debug: $tag, $value\n"; 

business-isp/lib/Business/ISP/GUI/Accounting.pm:

confirm_payment:
    Line num: 1312, Line Data:  my $date = $self->string_date();
    Line num: 1316, Line Data:  $self->pb_param( date => $date );
display_invoice:
    Line num: 1867, Line Data:  my $date = $invoice->[0]->{ date };
    Line num: 1928, Line Data:  $template->param( date => $date );