Home | History | Annotate | Download | only in lib
      1 #!/usr/bin/perl
      2 #***************************************************************************
      3 #                                  _   _ ____  _
      4 #  Project                     ___| | | |  _ \| |
      5 #                             / __| | | | |_) | |
      6 #                            | (__| |_| |  _ <| |___
      7 #                             \___|\___/|_| \_\_____|
      8 #
      9 # Copyright (C) 2011 - 2016, Daniel Stenberg, <daniel (at] haxx.se>, et al.
     10 #
     11 # This software is licensed as described in the file COPYING, which
     12 # you should have received as part of this distribution. The terms
     13 # are also available at https://curl.haxx.se/docs/copyright.html.
     14 #
     15 # You may opt to use, copy, modify, merge, publish, distribute and/or sell
     16 # copies of the Software, and permit persons to whom the Software is
     17 # furnished to do so, under the terms of the COPYING file.
     18 #
     19 # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
     20 # KIND, either express or implied.
     21 #
     22 ###########################################################################
     23 
     24 my $max_column = 79;
     25 my $indent = 2;
     26 
     27 my $warnings;
     28 my $errors;
     29 my $supressed; # whitelisted problems
     30 my $file;
     31 my $dir=".";
     32 my $wlist;
     33 my $windows_os = $^O eq 'MSWin32' || $^O eq 'msys' || $^O eq 'cygwin';
     34 my $verbose;
     35 my %whitelist;
     36 
     37 my %warnings = (
     38     'LONGLINE' =>         "Line longer than $max_column",
     39     'TABS' =>             'TAB characters not allowed',
     40     'TRAILINGSPACE' =>    'Trailing white space on the line',
     41     'CPPCOMMENTS' =>      '// comment detected',
     42     'SPACEBEFOREPAREN' => 'space before an open parenthesis',
     43     'SPACEAFTERPAREN'  => 'space after open parenthesis',
     44     'SPACEBEFORECLOSE' => 'space before a close parenthesis',
     45     'SPACEBEFORECOMMA' => 'space before a comma',
     46     'RETURNNOSPACE'    => 'return without space',
     47     'COMMANOSPACE'     => 'comma without following space',
     48     'BRACEELSE'        => '} else on the same line',
     49     'PARENBRACE'       => '){ without sufficient space',
     50     'SPACESEMILCOLON'  => 'space before semicolon',
     51     'BANNEDFUNC'       => 'a banned function was used',
     52     'FOPENMODE'        => 'fopen needs a macro for the mode string',
     53     'BRACEPOS'         => 'wrong position for an open brace',
     54     'INDENTATION'      => 'wrong start column for code',
     55     'COPYRIGHT'        => 'file missing a copyright statement',
     56     'BADCOMMAND'       => 'bad !checksrc! instruction',
     57     'UNUSEDIGNORE'     => 'a warning ignore was not used',
     58     'OPENCOMMENT'      => 'file ended with a /* comment still "open"',
     59     'ASTERISKSPACE'    => 'pointer declared with space after asterisk',
     60     'ASTERISKNOSPACE'  => 'pointer declared without space before asterisk'
     61     );
     62 
     63 sub readwhitelist {
     64     open(W, "<$dir/checksrc.whitelist");
     65     my @all=<W>;
     66     for(@all) {
     67         $windows_os ? $_ =~ s/\r?\n$// : chomp;
     68         $whitelist{$_}=1;
     69     }
     70     close(W);
     71 }
     72 
     73 sub checkwarn {
     74     my ($name, $num, $col, $file, $line, $msg, $error) = @_;
     75 
     76     my $w=$error?"error":"warning";
     77     my $nowarn=0;
     78 
     79     #if(!$warnings{$name}) {
     80     #    print STDERR "Dev! there's no description for $name!\n";
     81     #}
     82 
     83     # checksrc.whitelist
     84     if($whitelist{$line}) {
     85         $nowarn = 1;
     86     }
     87     # !checksrc! controlled
     88     elsif($ignore{$name}) {
     89         $ignore{$name}--;
     90         $ignore_used{$name}++;
     91         $nowarn = 1;
     92         if(!$ignore{$name}) {
     93             # reached zero, enable again
     94             enable_warn($name, $line, $file, $l);
     95         }
     96     }
     97 
     98     if($nowarn) {
     99         $supressed++;
    100         if($w) {
    101             $swarnings++;
    102         }
    103         else {
    104             $serrors++;
    105         }
    106         return;
    107     }
    108 
    109     if($w) {
    110         $warnings++;
    111     }
    112     else {
    113         $errors++;
    114     }
    115 
    116     $col++;
    117     print "$file:$num:$col: $w: $msg ($name)\n";
    118     print " $line\n";
    119 
    120     if($col < 80) {
    121         my $pref = (' ' x $col);
    122         print "${pref}^\n";
    123     }
    124 }
    125 
    126 $file = shift @ARGV;
    127 
    128 while(1) {
    129 
    130     if($file =~ /-D(.*)/) {
    131         $dir = $1;
    132         $file = shift @ARGV;
    133         next;
    134     }
    135     elsif($file =~ /-W(.*)/) {
    136         $wlist .= " $1 ";
    137         $file = shift @ARGV;
    138         next;
    139     }
    140     elsif($file =~ /^(-h|--help)/) {
    141         undef $file;
    142         last;
    143     }
    144 
    145     last;
    146 }
    147 
    148 if(!$file) {
    149     print "checksrc.pl [option] <file1> [file2] ...\n";
    150     print " Options:\n";
    151     print "  -D[DIR]   Directory to prepend file names\n";
    152     print "  -h        Show help output\n";
    153     print "  -W[file]  Whitelist the given file - ignore all its flaws\n";
    154     print "\nDetects and warns for these problems:\n";
    155     for(sort keys %warnings) {
    156         printf (" %-18s: %s\n", $_, $warnings{$_});
    157     }
    158     exit;
    159 }
    160 
    161 readwhitelist();
    162 
    163 do {
    164     if("$wlist" !~ / $file /) {
    165         my $fullname = $file;
    166         $fullname = "$dir/$file" if ($fullname !~ '^\.?\.?/');
    167         scanfile($fullname);
    168     }
    169     $file = shift @ARGV;
    170 
    171 } while($file);
    172 
    173 sub checksrc_clear {
    174     undef %ignore;
    175     undef %ignore_set;
    176     undef @ignore_line;
    177 }
    178 
    179 sub checksrc_endoffile {
    180     my ($file) = @_;
    181     for(keys %ignore_set) {
    182         if($ignore_set{$_} && !$ignore_used{$_}) {
    183             checkwarn("UNUSEDIGNORE", $ignore_set{$_},
    184                       length($_)+11, $file,
    185                       $ignore_line[$ignore_set{$_}],
    186                       "Unused ignore: $_");
    187         }
    188     }
    189 }
    190 
    191 sub enable_warn {
    192     my ($what, $line, $file, $l) = @_;
    193 
    194     # switch it back on, but warn if not triggered!
    195     if(!$ignore_used{$what}) {
    196         checkwarn("UNUSEDIGNORE",
    197                   $line, length($what) + 11, $file, $l,
    198                   "No warning was inhibited!");
    199     }
    200     $ignore_set{$what}=0;
    201     $ignore_used{$what}=0;
    202     $ignore{$what}=0;
    203 }
    204 sub checksrc {
    205     my ($cmd, $line, $file, $l) = @_;
    206     if($cmd =~ / *([^ ]*) *(.*)/) {
    207         my ($enable, $what) = ($1, $2);
    208         $what =~ s: *\*/$::; # cut off end of C comment
    209         # print "ENABLE $enable WHAT $what\n";
    210         if($enable eq "disable") {
    211             my ($warn, $scope)=($1, $2);
    212             if($what =~ /([^ ]*) +(.*)/) {
    213                 ($warn, $scope)=($1, $2);
    214             }
    215             else {
    216                 $warn = $what;
    217                 $scope = 1;
    218             }
    219             # print "IGNORE $warn for SCOPE $scope\n";
    220             if($scope eq "all") {
    221                 $scope=999999;
    222             }
    223 
    224             if($ignore_set{$warn}) {
    225                 checkwarn("BADCOMMAND",
    226                           $line, 0, $file, $l,
    227                           "$warn already disabled from line $ignore_set{$warn}");
    228             }
    229             else {
    230                 $ignore{$warn}=$scope;
    231                 $ignore_set{$warn}=$line;
    232                 $ignore_line[$line]=$l;
    233             }
    234         }
    235         elsif($enable eq "enable") {
    236             enable_warn($what, $line, $file, $l);
    237         }
    238         else {
    239             checkwarn("BADCOMMAND",
    240                       $line, 0, $file, $l,
    241                       "Illegal !checksrc! command");
    242         }
    243     }
    244 }
    245 
    246 sub scanfile {
    247     my ($file) = @_;
    248 
    249     my $line = 1;
    250     my $prevl;
    251     my $l;
    252     open(R, "<$file") || die "failed to open $file";
    253 
    254     my $incomment=0;
    255     my $copyright=0;
    256     checksrc_clear(); # for file based ignores
    257 
    258     while(<R>) {
    259         $windows_os ? $_ =~ s/\r?\n$// : chomp;
    260         my $l = $_;
    261         my $ol = $l; # keep the unmodified line for error reporting
    262         my $column = 0;
    263 
    264         # check for !checksrc! commands
    265         if($l =~ /\!checksrc\! (.*)/) {
    266             my $cmd = $1;
    267             checksrc($cmd, $line, $file, $l)
    268         }
    269 
    270         # check for a copyright statement
    271         if(!$copyright && ($l =~ /copyright .* \d\d\d\d/i)) {
    272             $copyright=1;
    273         }
    274 
    275         # detect long lines
    276         if(length($l) > $max_column) {
    277             checkwarn("LONGLINE", $line, length($l), $file, $l,
    278                       "Longer than $max_column columns");
    279         }
    280         # detect TAB characters
    281         if($l =~ /^(.*)\t/) {
    282             checkwarn("TABS",
    283                       $line, length($1), $file, $l, "Contains TAB character", 1);
    284         }
    285         # detect trailing white space
    286         if($l =~ /^(.*)[ \t]+\z/) {
    287             checkwarn("TRAILINGSPACE",
    288                       $line, length($1), $file, $l, "Trailing whitespace");
    289         }
    290 
    291         # ------------------------------------------------------------
    292         # Above this marker, the checks were done on lines *including*
    293         # comments
    294         # ------------------------------------------------------------
    295 
    296         # strip off C89 comments
    297 
    298       comment:
    299         if(!$incomment) {
    300             if($l =~ s/\/\*.*\*\// /g) {
    301                 # full /* comments */ were removed!
    302             }
    303             if($l =~ s/\/\*.*//) {
    304                 # start of /* comment was removed
    305                 $incomment = 1;
    306             }
    307         }
    308         else {
    309             if($l =~ s/.*\*\///) {
    310                 # end of comment */ was removed
    311                 $incomment = 0;
    312                 goto comment;
    313             }
    314             else {
    315                 # still within a comment
    316                 $l="";
    317             }
    318         }
    319 
    320         # ------------------------------------------------------------
    321         # Below this marker, the checks were done on lines *without*
    322         # comments
    323         # ------------------------------------------------------------
    324 
    325         # crude attempt to detect // comments without too many false
    326         # positives
    327         if($l =~ /^([^"\*]*)[^:"]\/\//) {
    328             checkwarn("CPPCOMMENTS",
    329                       $line, length($1), $file, $l, "\/\/ comment");
    330         }
    331 
    332         # check spaces after for/if/while
    333         if($l =~ /^(.*)(for|if|while) \(/) {
    334             if($1 =~ / *\#/) {
    335                 # this is a #if, treat it differently
    336             }
    337             else {
    338                 checkwarn("SPACEBEFOREPAREN", $line, length($1)+length($2), $file, $l,
    339                           "$2 with space");
    340             }
    341         }
    342 
    343         # check spaces after open parentheses
    344         if($l =~ /^(.*[a-z])\( /i) {
    345             checkwarn("SPACEAFTERPAREN",
    346                       $line, length($1)+1, $file, $l,
    347                       "space after open parenthesis");
    348         }
    349 
    350         # check spaces before close parentheses, unless it was a space or a
    351         # close parenthesis!
    352         if($l =~ /(.*[^\) ]) \)/) {
    353             checkwarn("SPACEBEFORECLOSE",
    354                       $line, length($1)+1, $file, $l,
    355                       "space before close parenthesis");
    356         }
    357 
    358         # check spaces before comma!
    359         if($l =~ /(.*[^ ]) ,/) {
    360             checkwarn("SPACEBEFORECOMMA",
    361                       $line, length($1)+1, $file, $l,
    362                       "space before comma");
    363         }
    364 
    365         # check for "return(" without space
    366         if($l =~ /^(.*)return\(/) {
    367             if($1 =~ / *\#/) {
    368                 # this is a #if, treat it differently
    369             }
    370             else {
    371                 checkwarn("RETURNNOSPACE", $line, length($1)+6, $file, $l,
    372                           "return without space before paren");
    373             }
    374         }
    375 
    376         # check for comma without space
    377         if($l =~ /^(.*),[^ \n]/) {
    378             my $pref=$1;
    379             my $ign=0;
    380             if($pref =~ / *\#/) {
    381                 # this is a #if, treat it differently
    382                 $ign=1;
    383             }
    384             elsif($pref =~ /\/\*/) {
    385                 # this is a comment
    386                 $ign=1;
    387             }
    388             elsif($pref =~ /[\"\']/) {
    389                 $ign = 1;
    390                 # There is a quote here, figure out whether the comma is
    391                 # within a string or '' or not.
    392                 if($pref =~ /\"/) {
    393                     # withing a string
    394                 }
    395                 elsif($pref =~ /\'$/) {
    396                     # a single letter
    397                 }
    398                 else {
    399                     $ign = 0;
    400                 }
    401             }
    402             if(!$ign) {
    403                 checkwarn("COMMANOSPACE", $line, length($pref)+1, $file, $l,
    404                           "comma without following space");
    405             }
    406         }
    407 
    408         # check for "} else"
    409         if($l =~ /^(.*)\} *else/) {
    410             checkwarn("BRACEELSE",
    411                       $line, length($1), $file, $l, "else after closing brace on same line");
    412         }
    413         # check for "){"
    414         if($l =~ /^(.*)\)\{/) {
    415             checkwarn("PARENBRACE",
    416                       $line, length($1)+1, $file, $l, "missing space after close paren");
    417         }
    418 
    419         # check for space before the semicolon last in a line
    420         if($l =~ /^(.*[^ ].*) ;$/) {
    421             checkwarn("SPACESEMILCOLON",
    422                       $line, length($1), $file, $ol, "space before last semicolon");
    423         }
    424 
    425         # scan for use of banned functions
    426         if($l =~ /^(.*\W)
    427                    (gets|
    428 	            strtok|
    429                     v?sprintf|
    430                     (str|_mbs|_tcs|_wcs)n?cat|
    431                     LoadLibrary(Ex)?(A|W)?)
    432                    \s*\(
    433                  /x) {
    434             checkwarn("BANNEDFUNC",
    435                       $line, length($1), $file, $ol,
    436                       "use of $2 is banned");
    437         }
    438 
    439         # scan for use of non-binary fopen without the macro
    440         if($l =~ /^(.*\W)fopen\s*\([^,]*, *\"([^"]*)/) {
    441             my $mode = $2;
    442             if($mode !~ /b/) {
    443                 checkwarn("FOPENMODE",
    444                           $line, length($1), $file, $ol,
    445                           "use of non-binary fopen without FOPEN_* macro: $mode");
    446             }
    447         }
    448 
    449         # check for open brace first on line but not first column
    450         # only alert if previous line ended with a close paren and wasn't a cpp
    451         # line
    452         if((($prevl =~ /\)\z/) && ($prevl !~ /^ *#/)) && ($l =~ /^( +)\{/)) {
    453             checkwarn("BRACEPOS",
    454                       $line, length($1), $file, $ol, "badly placed open brace");
    455         }
    456 
    457         # if the previous line starts with if/while/for AND ends with an open
    458         # brace, check that this line is indented $indent more steps, if not
    459         # a cpp line
    460         if($prevl =~ /^( *)(if|while|for)\(.*\{\z/) {
    461             my $first = length($1);
    462 
    463             # this line has some character besides spaces
    464             if(($l !~ /^ *#/) && ($l =~ /^( *)[^ ]/)) {
    465                 my $second = length($1);
    466                 my $expect = $first+$indent;
    467                 if($expect != $second) {
    468                     my $diff = $second - $first;
    469                     checkwarn("INDENTATION", $line, length($1), $file, $ol,
    470                               "not indented $indent steps, uses $diff)");
    471 
    472                 }
    473             }
    474         }
    475 
    476         # check for 'char * name'
    477         if(($l =~ /(^.*(char|int|long|void|curl_slist|CURL|CURLM|CURLMsg|curl_httppost) *(\*+)) (\w+)/) && ($4 ne "const")) {
    478             checkwarn("ASTERISKNOSPACE",
    479                       $line, length($1), $file, $ol,
    480                       "no space after declarative asterisk");
    481         }
    482         # check for 'char*'
    483         if(($l =~ /(^.*(char|int|long|void|curl_slist|CURL|CURLM|CURLMsg|curl_httppost|sockaddr_in|FILE)\*)/)) {
    484             checkwarn("ASTERISKNOSPACE",
    485                       $line, length($1)-1, $file, $ol,
    486                       "no space before asterisk");
    487         }
    488 
    489         # check for 'void func() {', but avoid false positives by requiring
    490         # both an open and closed parentheses before the open brace
    491         if($l =~ /^((\w).*){\z/) {
    492             my $k = $1;
    493             $k =~ s/const *//;
    494             $k =~ s/static *//;
    495             if($k =~ /\(.*\)/) {
    496                 checkwarn("BRACEPOS",
    497                           $line, length($l)-1, $file, $ol,
    498                           "wrongly placed open brace");
    499             }
    500         }
    501         $line++;
    502         $prevl = $ol;
    503     }
    504 
    505     if(!$copyright) {
    506         checkwarn("COPYRIGHT", 1, 0, $file, "", "Missing copyright statement", 1);
    507     }
    508     if($incomment) {
    509         checkwarn("OPENCOMMENT", 1, 0, $file, "", "Missing closing comment", 1);
    510     }
    511 
    512     checksrc_endoffile($file);
    513 
    514     close(R);
    515 
    516 }
    517 
    518 
    519 if($errors || $warnings || $verbose) {
    520     printf "checksrc: %d errors and %d warnings\n", $errors, $warnings;
    521     if($supressed) {
    522         printf "checksrc: %d errors and %d warnings suppressed\n",
    523         $serrors,
    524         $swarnings;
    525     }
    526     exit 5; # return failure
    527 }
    528