#!/usr/bin/env perl package OpenTracing::WrapScope::ConfigGenerator; use strict; use warnings; use autodie; use feature qw/say state/; use Carp qw/croak/; use Getopt::Long qw/GetOptionsFromArray/; use IO::File; 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/; use YAML::XS; 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 L 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