branch: master
badwords
7484 bytesRaw
#!/usr/bin/env perl
# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
#
# SPDX-License-Identifier: curl
#
# bad[:=]correct
#
# If separator is '=', the string will be compared case sensitively.
# If separator is ':', the check is done case insensitively.
#
# To add white listed uses of bad words that are removed before checking for
# the bad ones:
#
# ---(accepted word)
# ---:[path]:(accepted word)
#
use strict;
use warnings;
use File::Basename;
#
## States
#
# 0 - default, initial state
# 1 - there was a slash
# 2 - quoted string
# 3 - // comment
# 4 - /* comment
# 5 - asterisk found within a /* comment
# 6 - #include line
# 7 - backslash in a string
#
## Flags
#
# 1 - include preprocessor line, ignore strings
sub srcline {
my ($state, $flags, $l) = @_;
my $line = "";
if(($state == 0) && ($l =~ /^ *\# *include/)) {
# preprocessor include line
$flags |= 1;
}
else {
# not preprocessor
$flags &= ~1;
}
if($state == 3) {
# // ended on the prev line, go back to init
$state = 0;
}
my @c = split(//, $l);
# state machine this line
for my $c (@c) {
if($state == 1) {
# we had a slash
if($c eq "/") {
# // confirmed, the rest of the line is a comment
$line .= "//";
$state = 3;
}
elsif($c eq "*") {
# /* confirmed
$state = 4;
$line .= "/*";
}
else {
# back to normal
$line .= " ";
$state = 0;
}
}
elsif($state == 2) {
# a string
if($c eq "\\") {
$line .= "\\";
$state = 7;
}
elsif($c eq "\"") {
# end of the string
$line .= "\"";
$state = 0;
}
else {
$line .= $c;
}
}
elsif($state == 3) {
# a // comment
$line .= $c;
}
elsif($state == 4) {
# a /* comment
if($c eq "*") {
# could be a comment close
$state = 5;
}
else {
$line .= $c;
}
}
elsif($state == 5) {
if($c eq "/") {
# a /* */ comment ended here */
$line .= "*/";
$state = 0;
}
else {
# the /* comment continues
$line .= "*$c";
$state = 4;
}
}
elsif($state == 7) {
# the prev was a backslash in a string
$line .= $c;
# switch back to normal string
$state = 2;
}
else {
if($c eq "/") {
$state = 1; # got a slash
}
elsif(($c eq "\"") && !($flags & 1)) {
# start of a string, not within a preprocessor line
$line .= "\"";
$state = 2;
}
elsif($c eq "\n") {
$line .= "\n";
}
else {
$line .= " ";
}
}
}
return $state, $flags, $line;
}
sub sourcecode {
my ($f) = @_;
my $state = 0;
my $flags = 0;
my @lines;
my $line;
open(F, "<$f");
while(<F>) {
my $l = $_;
($state, $flags, $line) = srcline($state, $flags, $l);
push @lines, $line;
}
close(F);
return @lines;
}
my @whitelist = (
# ignore what looks like URLs
'(^|\W)((https|http|ftp):\/\/[a-z0-9\-._~%:\/?\#\[\]\@!\$&\'\(\)*+,;=]+)',
# remove bolded sections
'\*\*.*?\*\*',
# remove backticked texts
'\`.*?\`'
);
my %alt;
my %exactcase;
my %wl;
my @w;
my @exact;
my $file = shift @ARGV;
open(CONFIG, "<$file") or die "Cannot open '$file': $!";
while(<CONFIG>) {
chomp;
if($_ =~ /^#/) {
next;
}
if(/^---:([^:]*):(.*)/) {
# whitelist file + word
my $word = lc($2);
$wl{"$1:$word"}=1;
}
elsif($_ =~ /^---(.+)/) {
# whitelist word
push @whitelist, $1;
}
elsif($_ =~ /^(.*)([:=])(.*)/) {
my ($bad, $sep, $better)=($1, $2, $3);
if($sep eq "=") {
$alt{$bad} = $better;
push @exact, $bad;
}
else {
$alt{lc($bad)} = $better;
push @w, $bad;
}
}
}
close(CONFIG);
# Build a single combined regex for case-insensitive words
my $re_ci;
if(@w) {
my $pat = join('|', map { quotemeta($_) } @w);
$re_ci = qr/\b($pat)\b/i;
}
# Build a single combined regex for case-sensitive (exact) words
my $re_cs;
if(@exact) {
my $pat = join('|', map { quotemeta($_) } @exact);
$re_cs = qr/\b($pat)\b/;
}
# Build a single combined regex for removing whitelisted content
my $re_wl;
my $pat = join('|', map { $_ } @whitelist);
$re_wl = qr/($pat)/;
my $errors = 0;
sub highlight {
my ($p, $w, $in, $f, $l, $lookup) = @_;
my $c = length($p)+1;
my $ch;
my $dir = dirname($f);
$ch = $dir . "/" . ":" . lc($w);
if($wl{$ch}) {
# whitelisted dirname + word
return;
}
my $updir = dirname($dir);
if($dir ne $updir) {
$ch = $updir . "/" . ":" . lc($w);
if($wl{$ch}) {
# whitelisted upper dirname + word
return;
}
}
$ch = $f . ":" . lc($w);
if($wl{$ch}) {
# whitelisted filename + word
return;
}
print STDERR "$f:$l:$c: error: found bad word \"$w\"\n";
printf STDERR " %4d | %s\n", $l, $in;
printf STDERR " | %*s^%s\n", length($p), " ",
"~" x (length($w)-1);
printf STDERR " maybe use \"%s\" instead?\n", $alt{$lookup};
$errors++;
}
sub document {
my ($f) = @_;
my @lines;
open(F, "<$f");
while(<F>) {
push @lines, $_;
}
close(F);
return @lines;
}
sub file {
my ($f) = @_;
my $l = 0;
my $skip_indented = 0;
my $source_code = 0;
if($f =~ /\.[ch]$/) {
$source_code = 1;
}
else {
# markdown
$skip_indented = 1;
}
my @lines;
if($source_code) {
@lines = sourcecode($f);
}
else {
@lines = document($f);
}
for my $in (@lines) {
$l++;
chomp $in;
if($skip_indented && $in =~ /^ /) {
next;
}
# remove the link part
$in =~ s/(\[.*\])\(.*\)/$1/g;
# remove whitelisted patterns (pre-compiled)
if($re_wl) {
$in =~ s/${re_wl}//ig;
}
# case-insensitive bad words
if($re_ci) {
if($in =~ /^(.*)$re_ci/i) {
highlight($1, $2, $in, $f, $l, lc($2));
}
}
# case-sensitive (exact) bad words
if($re_cs) {
if($in =~ /^(.*)$re_cs/) {
highlight($1, $2, $in, $f, $l, $2);
}
}
}
}
my @filemasks = @ARGV;
open(my $git_ls_files, '-|', 'git', 'ls-files', '--', @filemasks) or die "Failed running git ls-files: $!";
my @files;
while(my $each = <$git_ls_files>) {
chomp $each;
push @files, $each;
}
close $git_ls_files;
my $onum = scalar(@files);
my $num;
for my $e (@files) {
#printf STDERR "Complete: %d%%\r", $num++ * 100 / $onum;
file($e);
}
exit $errors;