#!/usr/bin/env perl
use strict;
use autodie;
use feature qw/say state/;
use Carp qw/croak/;
use Getopt::Long qw/GetOptionsFromArray/;
use List::MoreUtils qw/notall/;
use List::Util qw/uniq/;
use PPI;
use Perl::Critic::Utils::McCabe qw/calculate_mccabe_of_sub/;
use Pod::Usage qw/pod2usage/;
exit run(@ARGV) unless caller;
sub run {
pod2usage -verbose => 1, -noperldoc => 1 if not @_;
GetOptionsFromArray(\@_,
'spec=s' => \my $spec_file,
'file=s' => \my @files,
'ignore=s' => \my @ignore,
'include=s' => \my @include,
'exclude=s' => \my @exclude,
'filter=s' => \my @filters,
'out=s' => \my $output_file,
'help' => \my $help,
) or pod2usage (
-verbose => 1,
-noperldoc => 1,
-msg => 'Invalid options',
);
pod2usage -verbose => 1, -noperldoc => 1 if $help;
my %args;
%args = %{ YAML::XS::LoadFile($spec_file) } if defined $spec_file;
push @{ $args{files} }, (@files, @{ $args{file} // [] });
push @{ $args{filters} }, (@filters, @{ $args{filter} // [] });
push @{ $args{ignore} }, @ignore;
push @{ $args{include} }, @include;
push @{ $args{exclude} }, @exclude;
my @subs = OpenTracing::WrapScope::ConfigGenerator::examine_files(%args);
open my $fh_out, '>', $output_file if $output_file;
$fh_out //= \*STDOUT;
say {$fh_out} $_ foreach @subs;
return 0;
}
sub _generate_filters {
my ($filter_specs) = @_;
return if not $filter_specs;
state $GENERATORS = {
exclude_private => sub {
return sub {
my ($sub) = @_;
return $sub->name !~ /(?:\A|::)_\w+\z/;
};
},
complexity => sub {
my ($threshold) = @_;
croak 'No arguments for complexity filter' if not $threshold;
return sub {
my ($sub) = @_;
return calculate_mccabe_of_sub($sub) >= $threshold;
};
},
};
my @filters;
foreach (@$filter_specs) {
my ($filter, $arg) = split /=/;
my $generator = $GENERATORS->{$filter} or croak "No such filter: $_";
push @filters, $generator->($arg);
}
return \@filters;
}
sub examine_files {
my %args = @_;
my $files_base = $args{files} // [];
my $files_ignore = $args{ignore} // [];
my $subs_include = $args{include} // [];
my $subs_exclude = $args{exclude} // [];
my $filters = _generate_filters($args{filters});
my @files = grep { -f } map { glob } @$files_base;
my %file_ignored = map { $_ => undef } map { glob } @$files_ignore;
my %sub_excluded = map { $_ => undef } @$subs_exclude;
my @subs = @$subs_include;
foreach my $file (@files) {
next if exists $file_ignored{$file};
foreach my $sub (list_subs($file, $filters)) {
next if exists $sub_excluded{$sub};
push @subs, $sub;
}
}
return uniq @subs;
}
sub list_subs {
my ($filename, $filters) = @_;
my $doc = PPI::Document->new($filename);
my $subs = $doc->find('PPI::Statement::Sub');
return if not $subs;
my @subs;
foreach my $sub (@$subs) {
next if notall { $_->($sub) } @$filters;
if ($sub->name =~ /'|::/) { # qualified
push @subs, $sub->name;
}
else {
my $pkg = _detect_package($sub);
$pkg = $pkg ? $pkg->namespace : 'main';
push @subs, $pkg . '::' . $sub->name;
}
}
return @subs;
}
sub _detect_package {
my ($elem) = @_;
return unless $elem;
return $elem if $elem->isa('PPI::Statement::Package');
my $prev = $elem;
while ($prev = $prev->sprevious_sibling) {
return $prev if $prev->isa('PPI::Statement::Package');
}
return _detect_package($elem->parent);
}
1;
__END__
=pod
=head1 NAME
opentracing_wrapscope_generator - generate input for OpenTracing::WrapScope
=head1 SYNOPSIS
opentracing_wrapscope_generator --out wrapscope.conf --file 'lib/*.pm'
opentracing_wrapscope_generator --file 'bin/*.pl' --filter complexity=5
=head1 OPTIONS
=head2 --file $file_pattern
Shell file pattern for files to include in the search.
Can be specified multiple times.
=head2 --ignore $file_pattern
Shell file pattern for files to ignore.
Can be specified multiple times.
=head2 --include $subroutine
A subroutine name to unconditionally include in the results.
Overrides all other settings.
Can be specified multiple times.
=head2 --exclude $subroutine
A subroutine name to remove from the results.
Can be specified multiple times.
=head2 --filter $filter_name[=$filter_argument]
A filter to apply. Currently supported filters:
=over 4
=item * exclude_private
Removes all subroutines starting with an underscore.
=item * complexity=INT
Narrows down the results to subroutines with a
greater than or equal than the argument. The argument is required.
=back
Can be specified multiple times.
=head2 --out $filename
The filename where the resulting config file should be written.
If this argument is not specified,
results will be printed to standard output.
=head2 --spec $filename
The filename of a YAML file which contains options for this program in hash form.
The keys correspond directly to options, most options can be specified multiple
times and their values should be arrays in the YAML file.
For example, given this file:
wrapscope_gen_config.yaml
file: [ 'bin/*.pl', 'lib/*.pm' ]
filter: [ 'exclude_private' ]
ignore: [ 'Private*' ]
out: wrapscope_config.txt
calling:
opentracing_wrapscope_generator --spec wrapscope_gen_config.yaml
is equivalent to:
opentracing_wrapscope_generator \
--file 'bin/*.pl' \
--file 'lib/*.pm' \
--filter exclude_private \
--out wrapscope_config.txt \
--ignore 'Private*'
if other options are specified alongside a spec file, they will be merged.
=head2 --help
Show this help.
=cut