-
Notifications
You must be signed in to change notification settings - Fork 39
/
Copy pathcriticWrapper.pl
132 lines (101 loc) · 5.43 KB
/
criticWrapper.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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 $/; <STDIN> };
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];
}