use strict; use warnings; use Getopt::Long qw( GetOptions ); use File::Spec (); use File::Basename (); use utf8; use Unicode::Normalize qw( NFKD ); use open qw(:std :utf8); my $sSource = do { local $/; }; if ( !eval{ require PPI; require Perl::Critic; 1} ){ print "\nSkipping Perl::Critic as it is not installed\n"; # Quit early is fine, but needs to happen after fully reading STDIN due to a pipe issue on MacOS. exit(0); } my ($file, $profile, $severity, $theme, $exclude, $include); GetOptions ("file=s" => \$file, "profile=s" => \$profile, "severity=s" => \$severity, "theme=s" => \$theme, "exclude=s" => \$exclude, "include=s" => \$include, ); die("Did not pass any source via stdin") if !defined($sSource); $profile = resolve_profile($profile); # Do not check for readability of the source $file since we never actually read it. Only checking the name for policy violations. print "Perlcritic on $file and using profile $profile \n"; $sSource =~ s/([^\x00-\x7F])/AsciiReplacementChar($1)/ge; $sSource = adjustForKeywords($sSource); my $doc = PPI::Document->new( \$sSource); $doc->{filename} = $file; my $exclude_ref = $exclude ? [$exclude] : [] ; my $include_ref = $include ? [$include] : [] ; my $critic = Perl::Critic->new( -profile => $profile, -severity => $severity, -theme => $theme, -exclude => $exclude_ref, -include => $include_ref); Perl::Critic::Violation::set_format("%s~|~%l~|~%c~|~%m~|~%p~||~"); my @violations = $critic->critique($doc); print "Perl Critic violations:\n"; foreach my $viol (@violations){ print "$viol\n"; } sub adjustForKeywords { # PPI can't handle Keywords like `async` or `method`. This is a couple of hacks to make it work. # Be careful about using \s in any substitutions since it'll match newlines and throw off the line count for reporting issues. $sSource = shift; # Change `async sub` to `sub`, and keep the word sub aligned where the line started. Also supports method and multi $sSource =~ s/^(\h*)(?:async\h+)?(?:multi\h+)?(?:method|sub)\h(?=\h*\w)/${1}sub /gm; # Another possible alignment. This was an attempt at keeping the name aligned. # $sSource =~ s/^(\h*)((?:async\h+)?)(method|sub)\h(?=\h*\w)/"$1" . (" " x (length($2) + length($3) - 3)) . "sub "/gme; if ($sSource =~ /^use\h+(?:Object::Pad|feature\h.*class.*|experimental\h.*class.*|Feature::Compat::Class)[\h;]/m){ # Object::Pad or the new corinna. Eventually needs to be updated with use v.?? when it becomes part of a feature bundle # Remove :isa statements since they trip Subroutines::ProhibitCallsToUndeclaredSubs. This regex is less robust (e.g. version declaration), so we'll remove "class" in a seperate one. $sSource =~ s/^(\h*class\h+[\w:]+\h+):\h*isa\(\h*[\w:]+\h*\)/$1/gm; # classes become packages (which they are) to support RequireExplicitPackage and RequireFilenameMatchesPackage $sSource =~ s/^(\h*)class\h(?=\h*\w)/${1}package /gm; # Should these be mangled? Subroutines::ProhibitBuiltinHomonyms triggers on these # ADJUST blocks and similar are not processed correctly since they aren't recognized. Important for Modules::RequireEndWithOne $sSource =~ s/^(\h*)(ADJUST|ADJUST\h+:params|ADJUSTPARAMS|BUILD)(?=\h*\s?(\{|\())/${1}sub $2/gm; # Change private sigil'd methods to regular subs. Single underscore would get caught by Subroutines::ProhibitUnusedPrivateSubroutines $sSource =~ s/^(\h*)method\h+\$(?=\w)/${1}sub /gm; # Remove param(name) from source, since they get confused for subs as well. $sSource =~ s/^(\h*field\h+[\$\@\%]\w+\s+):param\(\s*\w+\s*\)/${1}/gm; } return $sSource; } sub AsciiReplacementChar { # Tries to find ascii replacements for non-ascii characters. # Usually a horrible solution, but Perl::Critic otherwise crashes on unicode data my ( $sChar ) = @_; my $sSanitized= NFKD($sChar); $sSanitized =~ s/[^a-zA-Z]//g; if(length($sSanitized) >= 1){ # This path is decent. Basically strips accents and character modifiers. # Might turn 1 character into multiple (ligatures, roman numerals) return $sSanitized } # Far worse, but we still need a character. Map to a deterministic choice in A-Za-z. # Totally butchers the word, but allows critic to still find unused subs, duplicate hash keys, etc. my $ord = ord($sChar) % 52; return $ord < 26 ? chr($ord + 65) : chr($ord + 71); } sub resolve_profile { my $profile = shift; if ($profile){ return $profile if -f $profile; die("User specified Critic profile $profile not readable"); } return $ENV{'PERLCRITIC'} if $ENV{'PERLCRITIC'} && -r $ENV{'PERLCRITIC'}; if ( my $home_dir = find_home_dir() ) { $profile = File::Spec->catfile( $home_dir, '.perlcriticrc' ); return $profile if -f $profile; } $profile = File::Spec->catfile( File::Basename::dirname(__FILE__), 'defaultCriticProfile' ); die("Can't find Navigator's default profile $profile ?!") unless( -f $profile ); return $profile; } sub find_home_dir { # This logic is taken from File::HomeDir::Tiny (via Perl::Critic) return ($^O eq 'MSWin32') && ("$]" < 5.016) ## no critic ( Variables::ProhibitPunctuationVars ValuesAndExpressions::ProhibitMagicNumbers ValuesAndExpressions::ProhibitMismatchedOperators ) ? ($ENV{HOME} || $ENV{USERPROFILE}) : (<~>)[0]; }