package Perl::Critic::Command;
use 5.006001;
use strict;
use warnings;
use English qw< -no_match_vars >;
use Readonly;
use Getopt::Long qw< GetOptions >;
use List::Util qw< first max >;
use Pod::Usage qw< pod2usage >;
use Perl::Critic::Exception::Parse ();
use Perl::Critic::Utils qw<
:characters :severities policy_short_name
$DEFAULT_VERBOSITY $DEFAULT_VERBOSITY_WITH_FILE_NAME
>;
use Perl::Critic::Utils::Constants qw< $_MODULE_VERSION_TERM_ANSICOLOR >;
use Perl::Critic::Violation qw<>;
#-----------------------------------------------------------------------------
our $VERSION = '1.134';
#-----------------------------------------------------------------------------
use Exporter 'import';
Readonly::Array our @EXPORT_OK => qw< run >;
Readonly::Hash our %EXPORT_TAGS => (
all => [ @EXPORT_OK ],
);
#-----------------------------------------------------------------------------
Readonly::Scalar my $DEFAULT_VIOLATIONS_FOR_TOP => 20;
Readonly::Scalar my $EXIT_SUCCESS => 0;
Readonly::Scalar my $EXIT_NO_FILES => 1;
Readonly::Scalar my $EXIT_HAD_VIOLATIONS => 2;
Readonly::Scalar my $EXIT_HAD_FILE_PROBLEMS => 3;
#-----------------------------------------------------------------------------
my @files = ();
my $critic = undef;
my $output = \*STDOUT;
#-----------------------------------------------------------------------------
sub _out {
my @lines = @_;
return print {$output} @lines;
}
#-----------------------------------------------------------------------------
sub run {
my %options = _get_options();
@files = _get_input(@ARGV);
my ($violations, $had_error_in_file) = _critique(\%options, @files);
return $EXIT_HAD_FILE_PROBLEMS if $had_error_in_file;
return $EXIT_NO_FILES if not defined $violations;
return $EXIT_HAD_VIOLATIONS if $violations;
return $EXIT_SUCCESS;
}
#-----------------------------------------------------------------------------
sub _get_options {
my %opts = _parse_command_line();
_dispatch_special_requests( %opts );
_validate_options( %opts );
# Convert severity shortcut options. If multiple shortcuts
# are given, the lowest one wins. If an explicit --severity
# option has been given, then the shortcuts are ignored. The
# @SEVERITY_NAMES variable is exported by Perl::Critic::Utils.
$opts{-severity} ||= first { exists $opts{"-$_"} } @SEVERITY_NAMES;
$opts{-severity} ||= first { exists $opts{"-$_"} } ($SEVERITY_LOWEST .. $SEVERITY_HIGHEST);
# If --top is specified, default the severity level to 1, unless an
# explicit severity is defined. This provides us flexibility to
# report top-offenders across just some or all of the severity levels.
# We also default the --top count to twenty if none is given
if ( exists $opts{-top} ) {
$opts{-severity} ||= 1;
$opts{-top} ||= $DEFAULT_VIOLATIONS_FOR_TOP;
}
#Override profile, if --noprofile is specified
if ( exists $opts{-noprofile} ) {
$opts{-profile} = $EMPTY;
}
return %opts;
}
#-----------------------------------------------------------------------------
sub _parse_command_line {
my %opts;
my @opt_specs = _get_option_specification();
Getopt::Long::Configure('no_ignore_case');
GetOptions( \%opts, @opt_specs ) || pod2usage(); #Exits
# I've adopted the convention of using key-value pairs for
# arguments to most functions. And to increase legibility,
# I have also adopted the familiar command-line practice
# of denoting argument names with a leading dash (-).
my %dashed_opts = map { ( "-$_" => $opts{$_} ) } keys %opts;
return %dashed_opts;
}
#-----------------------------------------------------------------------------
sub _dispatch_special_requests {
my (%opts) = @_;
if ( $opts{-help} ) { pod2usage( -verbose => 0 ) } # Exits
if ( $opts{-options} ) { pod2usage( -verbose => 1 ) } # Exits
if ( $opts{-man} ) { pod2usage( -verbose => 2 ) } # Exits
if ( $opts{-version} ) { _display_version() } # Exits
if ( $opts{-list} ) { _render_all_policy_listing() } # Exits
if ( $opts{'-list-enabled'} ) { _render_policy_listing(%opts) } # Exits
if ( $opts{'-list-themes'} ) { _render_theme_listing() } # Exits
if ( $opts{'-profile-proto'} ) { _render_profile_prototype() } # Exits
if ( $opts{-doc} ) { _render_policy_docs( %opts ) } # Exits
return 1;
}
#-----------------------------------------------------------------------------
sub _validate_options {
my (%opts) = @_;
my $msg = $EMPTY;
if ( $opts{-noprofile} && $opts{-profile} ) {
$msg .= qq{Warning: Cannot use -noprofile with -profile option.\n};
}
if ( $opts{-verbose} && $opts{-verbose} !~ m{(?: \d+ | %[mfFlcCedrpPs] )}xms) {
$msg .= qq<Warning: --verbose arg "$opts{-verbose}" looks odd. >;
$msg .= qq<Perhaps you meant to say "--verbose 3 $opts{-verbose}."\n>;
}
if ( exists $opts{-top} && $opts{-top} < 0 ) {
$msg .= qq<Warning: --top argument "$opts{-top}" is negative. >;
$msg .= qq<Perhaps you meant to say "$opts{-top} --top".\n>;
}
if (
exists $opts{-severity}
&& (
$opts{-severity} < $SEVERITY_LOWEST
|| $opts{-severity} > $SEVERITY_HIGHEST
)
) {
$msg .= qq<Warning: --severity arg "$opts{-severity}" out of range. >;
$msg .= qq<Severities range from "$SEVERITY_LOWEST" (lowest) to >;
$msg .= qq<"$SEVERITY_HIGHEST" (highest).\n>;
}
if ( $msg ) {
pod2usage( -exitstatus => 1, -message => $msg, -verbose => 0); #Exits
}
return 1;
}
#-----------------------------------------------------------------------------
sub _get_input {
my @args = @_;
if ( !@args || (@args == 1 && $args[0] eq q{-}) ) {
# Reading code from STDIN. All the code is slurped into
# a string. PPI will barf if the string is just whitespace.
my $code_string = do { local $RS = undef; <> };
# Notice if STDIN was closed (pipe error, etc)
if ( ! defined $code_string ) {
$code_string = $EMPTY;
}
$code_string =~ m{ \S+ }xms || die qq{Nothing to critique.\n};
return \$code_string; #Convert to SCALAR ref for PPI
}
else {
# Test to make sure all the specified files or directories
# actually exist. If any one of them is bogus, then die.
if ( my $nonexistent = first { ! -e } @args ) {
my $msg = qq{No such file or directory: '$nonexistent'};
pod2usage( -exitstatus => 1, -message => $msg, -verbose => 0);
}
# Reading code from files or dirs. If argument is a file,
# then we process it as-is (even though it may not actually
# be Perl code). If argument is a directory, recursively
# search the directory for files that look like Perl code.
return map { (-d) ? Perl::Critic::Utils::all_perl_files($_) : $_ } @args;
}
}
#------------------------------------------------------------------------------
sub _critique {
my ( $opts_ref, @files_to_critique ) = @_;
@files_to_critique || die "No perl files were found.\n";
# Perl::Critic has lots of dependencies, so loading is delayed
# until it is really needed. This hack reduces startup time for
# doing other things like getting the version number or dumping
# the man page. Arguably, those things are pretty rare, but hey,
# why not save a few seconds if you can.
require Perl::Critic;
$critic = Perl::Critic->new( %{$opts_ref} );
$critic->policies() || die "No policies selected.\n";
_set_up_pager($critic->config()->pager());
my $number_of_violations = undef;
my $had_error_in_file = 0;
for my $file (@files_to_critique) {
eval {
my @violations = $critic->critique($file);
$number_of_violations += scalar @violations;
if (not $opts_ref->{'-statistics-only'}) {
_render_report( $file, $opts_ref, @violations )
}
1;
}
or do {
if ( my $exception = Perl::Critic::Exception::Parse->caught() ) {
$had_error_in_file = 1;
warn qq<Problem while critiquing "$file": $EVAL_ERROR\n>;
}
elsif ($EVAL_ERROR) {
# P::C::Exception::Fatal includes the stack trace in its
# stringification.
die qq<Fatal error while critiquing "$file": $EVAL_ERROR\n>;
}
else {
die qq<Fatal error while critiquing "$file". Unfortunately, >,
q<$@/$EVAL_ERROR >, ## no critic (RequireInterpolationOfMetachars)
qq<is empty, so the reason can't be shown.\n>;
}
}
}
if ( $opts_ref->{-statistics} or $opts_ref->{'-statistics-only'} ) {
my $stats = $critic->statistics();
_report_statistics( $opts_ref, $stats );
}
return $number_of_violations, $had_error_in_file;
}
#------------------------------------------------------------------------------
sub _render_report {
my ( $file, $opts_ref, @violations ) = @_;
# Only report the files, if asked.
my $number_of_violations = scalar @violations;
if ( $opts_ref->{'-files-with-violations'} ||
$opts_ref->{'-files-without-violations'} ) {
not ref $file
and $opts_ref->{$number_of_violations ? '-files-with-violations' :
'-files-without-violations'}
and _out "$file\n";
return $number_of_violations;
}
# Only report the number of violations, if asked.
if( $opts_ref->{-count} ){
ref $file || _out "$file: ";
_out "$number_of_violations\n";
return $number_of_violations;
}
# Hail all-clear unless we should shut up.
if( !@violations && !$opts_ref->{-quiet} ) {
ref $file || _out "$file ";
_out "source OK\n";
return 0;
}
# Otherwise, format and print violations
my $verbosity = $critic->config->verbose();
# $verbosity can be numeric or string, so use "eq" for comparison;
$verbosity =
($verbosity eq $DEFAULT_VERBOSITY && @files > 1)
? $DEFAULT_VERBOSITY_WITH_FILE_NAME
: $verbosity;
my $fmt = Perl::Critic::Utils::verbosity_to_format( $verbosity );
if (not -f $file) { $fmt =~ s< \%[fF] ><STDIN>xms; } #HACK!
Perl::Critic::Violation::set_format( $fmt );
my $color = $critic->config->color();
_out $color ? _colorize_by_severity(@violations) : @violations;
return $number_of_violations;
}
#-----------------------------------------------------------------------------
sub _set_up_pager {
my ($pager_command) = @_;
return if not $pager_command;
return if not _at_tty();
open my $pager, q<|->, $pager_command ## no critic (InputOutput::RequireBriefOpen)
or die qq<Unable to pipe to pager "$pager_command": $ERRNO\n>;
$output = $pager;
return;
}
#-----------------------------------------------------------------------------
sub _report_statistics {
my ($opts_ref, $statistics) = @_;
if (
not $opts_ref->{'-statistics-only'}
and (
$statistics->total_violations()
or not $opts_ref->{-quiet} and $statistics->modules()
)
) {
_out "\n"; # There's prior output that we want to separate from.
}
my $files = _commaify($statistics->modules());
my $subroutines = _commaify($statistics->subs());
my $statements = _commaify($statistics->statements_other_than_subs());
my $lines = _commaify($statistics->lines());
my $width = max map { length } $files, $subroutines, $statements;
_out sprintf "%*s %s.\n", $width, $files, 'files';
_out sprintf "%*s %s.\n", $width, $subroutines, 'subroutines/methods';
_out sprintf "%*s %s.\n", $width, $statements, 'statements';
my $lines_of_blank = _commaify( $statistics->lines_of_blank() );
my $lines_of_comment = _commaify( $statistics->lines_of_comment() );
my $lines_of_data = _commaify( $statistics->lines_of_data() );
my $lines_of_perl = _commaify( $statistics->lines_of_perl() );
my $lines_of_pod = _commaify( $statistics->lines_of_pod() );
$width =
max map { length }
$lines_of_blank, $lines_of_comment, $lines_of_data,
$lines_of_perl, $lines_of_pod;
_out sprintf "\n%s %s:\n", $lines, 'lines, consisting of';
_out sprintf " %*s %s.\n", $width, $lines_of_blank, 'blank lines';
_out sprintf " %*s %s.\n", $width, $lines_of_comment, 'comment lines';
_out sprintf " %*s %s.\n", $width, $lines_of_data, 'data lines';
_out sprintf " %*s %s.\n", $width, $lines_of_perl, 'lines of Perl code';
_out sprintf " %*s %s.\n", $width, $lines_of_pod, 'lines of POD';
my $average_sub_mccabe = $statistics->average_sub_mccabe();
if (defined $average_sub_mccabe) {
_out
sprintf
"\nAverage McCabe score of subroutines was %.2f.\n",
$average_sub_mccabe;
}
_out "\n";
_out _commaify($statistics->total_violations()), " violations.\n";
my $violations_per_file = $statistics->violations_per_file();
if (defined $violations_per_file) {
_out
sprintf
"Violations per file was %.3f.\n",
$violations_per_file;
}
my $violations_per_statement = $statistics->violations_per_statement();
if (defined $violations_per_statement) {
_out
sprintf
"Violations per statement was %.3f.\n",
$violations_per_statement;
}
my $violations_per_line = $statistics->violations_per_line_of_code();
if (defined $violations_per_line) {
_out
sprintf
"Violations per line of code was %.3f.\n",
$violations_per_line;
}
if ( $statistics->total_violations() ) {
_out "\n";
my %severity_violations = %{ $statistics->violations_by_severity() };
my @severities = reverse sort keys %severity_violations;
$width =
max
map { length _commaify( $severity_violations{$_} ) }
@severities;
foreach my $severity (@severities) {
_out
sprintf
"%*s severity %d violations.\n",
$width,
_commaify( $severity_violations{$severity} ),
$severity;
}
_out "\n";
my %policy_violations = %{ $statistics->violations_by_policy() };
my @policies = sort keys %policy_violations;
$width =
max
map { length _commaify( $policy_violations{$_} ) }
@policies;
foreach my $policy (@policies) {
_out
sprintf
"%*s violations of %s.\n",
$width,
_commaify($policy_violations{$policy}),
policy_short_name($policy);
}
}
return;
}
#-----------------------------------------------------------------------------
# Only works for integers.
sub _commaify {
my ( $number ) = @_;
while ($number =~ s/ \A ( [-+]? \d+ ) ( \d{3} ) /$1,$2/xms) {
# nothing
}
return $number;
}
#-----------------------------------------------------------------------------
sub _get_option_specification {
return qw<
5 4 3 2 1
version
brutal
count|C
cruel
doc=s
exclude=s@
force!
gentle
harsh
help|?|H
include=s@
list
list-enabled
list-themes
man
color|colour!
noprofile
only!
options
pager=s
profile|p=s
profile-proto
quiet
severity=i
single-policy|s=s
stern
statistics!
statistics-only!
profile-strictness=s
theme=s
top:i
allow-unsafe
verbose=s
color-severity-highest|colour-severity-highest|color-severity-5|colour-severity-5=s
color-severity-high|colour-severity-high|color-severity-4|colour-severity-4=s
color-severity-medium|colour-severity-medium|color-severity-3|colour-severity-3=s
color-severity-low|colour-severity-low|color-severity-2|colour-severity-2=s
color-severity-lowest|colour-severity-lowest|color-severity-1|colour-severity-1=s
files-with-violations|l
files-without-violations|L
program-extensions=s@
>;
}
#-----------------------------------------------------------------------------
sub _colorize_by_severity {
my @violations = @_;
return @violations if _this_is_windows() && !eval 'require Win32::Console::ANSI; 1';
return @violations if not eval {
require Term::ANSIColor;
Term::ANSIColor->VERSION( $_MODULE_VERSION_TERM_ANSICOLOR );
1;
};
my $config = $critic->config();
my %color_of = (
$SEVERITY_HIGHEST => $config->color_severity_highest(),
$SEVERITY_HIGH => $config->color_severity_high(),
$SEVERITY_MEDIUM => $config->color_severity_medium(),
$SEVERITY_LOW => $config->color_severity_low(),
$SEVERITY_LOWEST => $config->color_severity_lowest(),
);
return map { _colorize( "$_", $color_of{$_->severity()} ) } @violations;
}
#-----------------------------------------------------------------------------
sub _colorize {
my ($string, $color) = @_;
return $string if not defined $color;
return $string if $color eq $EMPTY;
# $terminator is a purely cosmetic change to make the color end at the end
# of the line rather than right before the next line. It is here because
# if you use background colors, some console windows display a little
# fragment of colored background before the next uncolored (or
# differently-colored) line.
my $terminator = chomp $string ? "\n" : $EMPTY;
return Term::ANSIColor::colored( $string, $color ) . $terminator;
}
#-----------------------------------------------------------------------------
sub _this_is_windows {
return 1 if $OSNAME =~ m/MSWin32/xms;
return 0;
}
#-----------------------------------------------------------------------------
sub _at_tty {
return -t STDOUT; ## no critic (ProhibitInteractiveTest);
}
#-----------------------------------------------------------------------------
sub _render_all_policy_listing {
# Force P-C parameters, to catch all Policies on this site
my %pc_params = (-profile => $EMPTY, -severity => $SEVERITY_LOWEST);
return _render_policy_listing( %pc_params );
}
#-----------------------------------------------------------------------------
sub _render_policy_listing {
my %pc_params = @_;
require Perl::Critic::PolicyListing;
require Perl::Critic;
my @policies = Perl::Critic->new( %pc_params )->policies();
my $listing = Perl::Critic::PolicyListing->new( -policies => \@policies );
_out $listing;
exit $EXIT_SUCCESS;
}
#-----------------------------------------------------------------------------
sub _render_theme_listing {
require Perl::Critic::ThemeListing;
require Perl::Critic;
my %pc_params = (-profile => $EMPTY, -severity => $SEVERITY_LOWEST);
my @policies = Perl::Critic->new( %pc_params )->policies();
my $listing = Perl::Critic::ThemeListing->new( -policies => \@policies );
_out $listing;
exit $EXIT_SUCCESS;
}
#-----------------------------------------------------------------------------
sub _render_profile_prototype {
require Perl::Critic::ProfilePrototype;
require Perl::Critic;
my %pc_params = (-profile => $EMPTY, -severity => $SEVERITY_LOWEST);
my @policies = Perl::Critic->new( %pc_params )->policies();
my $prototype = Perl::Critic::ProfilePrototype->new( -policies => \@policies );
_out $prototype;
exit $EXIT_SUCCESS;
}
#-----------------------------------------------------------------------------
sub _render_policy_docs {
my (%opts) = @_;
my $pattern = delete $opts{-doc};
require Perl::Critic;
$critic = Perl::Critic->new(%opts);
_set_up_pager($critic->config()->pager());
require Perl::Critic::PolicyFactory;
my @site_policies = Perl::Critic::PolicyFactory->site_policy_names();
my @matching_policies = grep { /$pattern/ixms } @site_policies;
# "-T" means don't send to pager
my @perldoc_output = map {`perldoc -T $_`} @matching_policies; ## no critic (ProhibitBacktick)
_out @perldoc_output;
exit $EXIT_SUCCESS;
}
#-----------------------------------------------------------------------------
sub _display_version {
_out "$VERSION\n";
exit $EXIT_SUCCESS;
}
#-----------------------------------------------------------------------------
1;
__END__
#-----------------------------------------------------------------------------
=pod
=for stopwords
Twitter
=head1 NAME
Perl::Critic::Command - Guts of L<perlcritic|perlcritic>.
=head1 SYNOPSIS
use Perl::Critic::Command qw< run >;
local @ARGV = qw< --statistics-only lib bin >;
run();
=head1 DESCRIPTION
This is the implementation of the L<perlcritic|perlcritic> command. You can use
this to run the command without going through a command interpreter.
=head1 INTERFACE SUPPORT
This is considered to be a public class. However, its interface is
experimental, and will likely change.
=head1 IMPORTABLE SUBROUTINES
=over
=item C<run()>
Does the equivalent of the L<perlcritic|perlcritic> command. Unfortunately, at
present, this doesn't take any parameters but uses C<@ARGV> to get its
input instead. Count on this changing; don't count on the current
interface.
=back
=head1 TO DO
Make C<run()> take parameters. The equivalent of C<@ARGV> should be
passed as a reference.
Turn this into an object.
=head1 AUTHOR
Jeffrey Ryan Thalhammer <jeff@imaginative-software.com>
=head1 COPYRIGHT
Copyright (c) 2005-2011 Imaginative Software Systems. All rights reserved.
This program is free software; you can redistribute it and/or modify
it under the same terms as Perl itself. The full text of this license
can be found in the LICENSE file included with this module.
=cut
##############################################################################
# Local Variables:
# mode: cperl
# cperl-indent-level: 4
# fill-column: 78
# indent-tabs-mode: nil
# c-indentation-style: bsd
# End:
# ex: set ts=8 sts=4 sw=4 tw=78 ft=perl expandtab shiftround :