Home | History | Annotate | Download | only in lib
      1 #!/usr/bin/env perl
      2 #***************************************************************************
      3 #                                  _   _ ____  _
      4 #  Project                     ___| | | |  _ \| |
      5 #                             / __| | | | |_) | |
      6 #                            | (__| |_| |  _ <| |___
      7 #                             \___|\___/|_| \_\_____|
      8 #
      9 # Copyright (C) 2011 - 2017, 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     'ASSIGNWITHINCONDITION'  => 'assignment within conditional expression',
     62     'EQUALSNOSPACE'    => 'equals sign without following space',
     63     'NOSPACEEQUALS'    => 'equals sign without preceeding space',
     64     'SEMINOSPACE'      => 'semicolon without following space',
     65     'MULTISPACE'       => 'multiple spaces used when not suitable',
     66     );
     67 
     68 sub readwhitelist {
     69     open(W, "<$dir/checksrc.whitelist");
     70     my @all=<W>;
     71     for(@all) {
     72         $windows_os ? $_ =~ s/\r?\n$// : chomp;
     73         $whitelist{$_}=1;
     74     }
     75     close(W);
     76 }
     77 
     78 sub checkwarn {
     79     my ($name, $num, $col, $file, $line, $msg, $error) = @_;
     80 
     81     my $w=$error?"error":"warning";
     82     my $nowarn=0;
     83 
     84     #if(!$warnings{$name}) {
     85     #    print STDERR "Dev! there's no description for $name!\n";
     86     #}
     87 
     88     # checksrc.whitelist
     89     if($whitelist{$line}) {
     90         $nowarn = 1;
     91     }
     92     # !checksrc! controlled
     93     elsif($ignore{$name}) {
     94         $ignore{$name}--;
     95         $ignore_used{$name}++;
     96         $nowarn = 1;
     97         if(!$ignore{$name}) {
     98             # reached zero, enable again
     99             enable_warn($name, $line, $file, $l);
    100         }
    101     }
    102 
    103     if($nowarn) {
    104         $supressed++;
    105         if($w) {
    106             $swarnings++;
    107         }
    108         else {
    109             $serrors++;
    110         }
    111         return;
    112     }
    113 
    114     if($w) {
    115         $warnings++;
    116     }
    117     else {
    118         $errors++;
    119     }
    120 
    121     $col++;
    122     print "$file:$num:$col: $w: $msg ($name)\n";
    123     print " $line\n";
    124 
    125     if($col < 80) {
    126         my $pref = (' ' x $col);
    127         print "${pref}^\n";
    128     }
    129 }
    130 
    131 $file = shift @ARGV;
    132 
    133 while(1) {
    134 
    135     if($file =~ /-D(.*)/) {
    136         $dir = $1;
    137         $file = shift @ARGV;
    138         next;
    139     }
    140     elsif($file =~ /-W(.*)/) {
    141         $wlist .= " $1 ";
    142         $file = shift @ARGV;
    143         next;
    144     }
    145     elsif($file =~ /^(-h|--help)/) {
    146         undef $file;
    147         last;
    148     }
    149 
    150     last;
    151 }
    152 
    153 if(!$file) {
    154     print "checksrc.pl [option] <file1> [file2] ...\n";
    155     print " Options:\n";
    156     print "  -D[DIR]   Directory to prepend file names\n";
    157     print "  -h        Show help output\n";
    158     print "  -W[file]  Whitelist the given file - ignore all its flaws\n";
    159     print "\nDetects and warns for these problems:\n";
    160     for(sort keys %warnings) {
    161         printf (" %-18s: %s\n", $_, $warnings{$_});
    162     }
    163     exit;
    164 }
    165 
    166 readwhitelist();
    167 
    168 do {
    169     if("$wlist" !~ / $file /) {
    170         my $fullname = $file;
    171         $fullname = "$dir/$file" if ($fullname !~ '^\.?\.?/');
    172         scanfile($fullname);
    173     }
    174     $file = shift @ARGV;
    175 
    176 } while($file);
    177 
    178 sub checksrc_clear {
    179     undef %ignore;
    180     undef %ignore_set;
    181     undef @ignore_line;
    182 }
    183 
    184 sub checksrc_endoffile {
    185     my ($file) = @_;
    186     for(keys %ignore_set) {
    187         if($ignore_set{$_} && !$ignore_used{$_}) {
    188             checkwarn("UNUSEDIGNORE", $ignore_set{$_},
    189                       length($_)+11, $file,
    190                       $ignore_line[$ignore_set{$_}],
    191                       "Unused ignore: $_");
    192         }
    193     }
    194 }
    195 
    196 sub enable_warn {
    197     my ($what, $line, $file, $l) = @_;
    198 
    199     # switch it back on, but warn if not triggered!
    200     if(!$ignore_used{$what}) {
    201         checkwarn("UNUSEDIGNORE",
    202                   $line, length($what) + 11, $file, $l,
    203                   "No warning was inhibited!");
    204     }
    205     $ignore_set{$what}=0;
    206     $ignore_used{$what}=0;
    207     $ignore{$what}=0;
    208 }
    209 sub checksrc {
    210     my ($cmd, $line, $file, $l) = @_;
    211     if($cmd =~ / *([^ ]*) *(.*)/) {
    212         my ($enable, $what) = ($1, $2);
    213         $what =~ s: *\*/$::; # cut off end of C comment
    214         # print "ENABLE $enable WHAT $what\n";
    215         if($enable eq "disable") {
    216             my ($warn, $scope)=($1, $2);
    217             if($what =~ /([^ ]*) +(.*)/) {
    218                 ($warn, $scope)=($1, $2);
    219             }
    220             else {
    221                 $warn = $what;
    222                 $scope = 1;
    223             }
    224             # print "IGNORE $warn for SCOPE $scope\n";
    225             if($scope eq "all") {
    226                 $scope=999999;
    227             }
    228 
    229             if($ignore_set{$warn}) {
    230                 checkwarn("BADCOMMAND",
    231                           $line, 0, $file, $l,
    232                           "$warn already disabled from line $ignore_set{$warn}");
    233             }
    234             else {
    235                 $ignore{$warn}=$scope;
    236                 $ignore_set{$warn}=$line;
    237                 $ignore_line[$line]=$l;
    238             }
    239         }
    240         elsif($enable eq "enable") {
    241             enable_warn($what, $line, $file, $l);
    242         }
    243         else {
    244             checkwarn("BADCOMMAND",
    245                       $line, 0, $file, $l,
    246                       "Illegal !checksrc! command");
    247         }
    248     }
    249 }
    250 
    251 sub nostrings {
    252     my ($str) = @_;
    253     $str =~ s/\".*\"//g;
    254     return $str;
    255 }
    256 
    257 sub scanfile {
    258     my ($file) = @_;
    259 
    260     my $line = 1;
    261     my $prevl;
    262     my $l;
    263     open(R, "<$file") || die "failed to open $file";
    264 
    265     my $incomment=0;
    266     my $copyright=0;
    267     checksrc_clear(); # for file based ignores
    268 
    269     while(<R>) {
    270         $windows_os ? $_ =~ s/\r?\n$// : chomp;
    271         my $l = $_;
    272         my $ol = $l; # keep the unmodified line for error reporting
    273         my $column = 0;
    274 
    275         # check for !checksrc! commands
    276         if($l =~ /\!checksrc\! (.*)/) {
    277             my $cmd = $1;
    278             checksrc($cmd, $line, $file, $l)
    279         }
    280 
    281         # check for a copyright statement
    282         if(!$copyright && ($l =~ /copyright .* \d\d\d\d/i)) {
    283             $copyright=1;
    284         }
    285 
    286         # detect long lines
    287         if(length($l) > $max_column) {
    288             checkwarn("LONGLINE", $line, length($l), $file, $l,
    289                       "Longer than $max_column columns");
    290         }
    291         # detect TAB characters
    292         if($l =~ /^(.*)\t/) {
    293             checkwarn("TABS",
    294                       $line, length($1), $file, $l, "Contains TAB character", 1);
    295         }
    296         # detect trailing white space
    297         if($l =~ /^(.*)[ \t]+\z/) {
    298             checkwarn("TRAILINGSPACE",
    299                       $line, length($1), $file, $l, "Trailing whitespace");
    300         }
    301 
    302         # ------------------------------------------------------------
    303         # Above this marker, the checks were done on lines *including*
    304         # comments
    305         # ------------------------------------------------------------
    306 
    307         # strip off C89 comments
    308 
    309       comment:
    310         if(!$incomment) {
    311             if($l =~ s/\/\*.*\*\// /g) {
    312                 # full /* comments */ were removed!
    313             }
    314             if($l =~ s/\/\*.*//) {
    315                 # start of /* comment was removed
    316                 $incomment = 1;
    317             }
    318         }
    319         else {
    320             if($l =~ s/.*\*\///) {
    321                 # end of comment */ was removed
    322                 $incomment = 0;
    323                 goto comment;
    324             }
    325             else {
    326                 # still within a comment
    327                 $l="";
    328             }
    329         }
    330 
    331         # ------------------------------------------------------------
    332         # Below this marker, the checks were done on lines *without*
    333         # comments
    334         # ------------------------------------------------------------
    335 
    336         # crude attempt to detect // comments without too many false
    337         # positives
    338         if($l =~ /^([^"\*]*)[^:"]\/\//) {
    339             checkwarn("CPPCOMMENTS",
    340                       $line, length($1), $file, $l, "\/\/ comment");
    341         }
    342 
    343         my $nostr = nostrings($l);
    344         # check spaces after for/if/while/function call
    345         if($nostr =~ /^(.*)(for|if|while| ([a-zA-Z0-9_]+)) \((.)/) {
    346             if($1 =~ / *\#/) {
    347                 # this is a #if, treat it differently
    348             }
    349             elsif($3 eq "return") {
    350                 # return must have a space
    351             }
    352             elsif($3 eq "case") {
    353                 # case must have a space
    354             }
    355             elsif($4 eq "*") {
    356                 # (* beginning makes the space OK!
    357             }
    358             elsif($1 =~ / *typedef/) {
    359                 # typedefs can use space-paren
    360             }
    361             else {
    362                 checkwarn("SPACEBEFOREPAREN", $line, length($1)+length($2), $file, $l,
    363                           "$2 with space");
    364             }
    365         }
    366 
    367         if($nostr =~ /^((.*)(if) *\()(.*)\)/) {
    368             my $pos = length($1);
    369             if($4 =~ / = /) {
    370                 checkwarn("ASSIGNWITHINCONDITION",
    371                           $line, $pos+1, $file, $l,
    372                           "assignment within conditional expression");
    373             }
    374         }
    375         # check spaces after open parentheses
    376         if($l =~ /^(.*[a-z])\( /i) {
    377             checkwarn("SPACEAFTERPAREN",
    378                       $line, length($1)+1, $file, $l,
    379                       "space after open parenthesis");
    380         }
    381 
    382         # check spaces before close parentheses, unless it was a space or a
    383         # close parenthesis!
    384         if($l =~ /(.*[^\) ]) \)/) {
    385             checkwarn("SPACEBEFORECLOSE",
    386                       $line, length($1)+1, $file, $l,
    387                       "space before close parenthesis");
    388         }
    389 
    390         # check spaces before comma!
    391         if($l =~ /(.*[^ ]) ,/) {
    392             checkwarn("SPACEBEFORECOMMA",
    393                       $line, length($1)+1, $file, $l,
    394                       "space before comma");
    395         }
    396 
    397         # check for "return(" without space
    398         if($l =~ /^(.*)return\(/) {
    399             if($1 =~ / *\#/) {
    400                 # this is a #if, treat it differently
    401             }
    402             else {
    403                 checkwarn("RETURNNOSPACE", $line, length($1)+6, $file, $l,
    404                           "return without space before paren");
    405             }
    406         }
    407 
    408         # check for comma without space
    409         if($l =~ /^(.*),[^ \n]/) {
    410             my $pref=$1;
    411             my $ign=0;
    412             if($pref =~ / *\#/) {
    413                 # this is a #if, treat it differently
    414                 $ign=1;
    415             }
    416             elsif($pref =~ /\/\*/) {
    417                 # this is a comment
    418                 $ign=1;
    419             }
    420             elsif($pref =~ /[\"\']/) {
    421                 $ign = 1;
    422                 # There is a quote here, figure out whether the comma is
    423                 # within a string or '' or not.
    424                 if($pref =~ /\"/) {
    425                     # withing a string
    426                 }
    427                 elsif($pref =~ /\'$/) {
    428                     # a single letter
    429                 }
    430                 else {
    431                     $ign = 0;
    432                 }
    433             }
    434             if(!$ign) {
    435                 checkwarn("COMMANOSPACE", $line, length($pref)+1, $file, $l,
    436                           "comma without following space");
    437             }
    438         }
    439 
    440         # check for "} else"
    441         if($l =~ /^(.*)\} *else/) {
    442             checkwarn("BRACEELSE",
    443                       $line, length($1), $file, $l, "else after closing brace on same line");
    444         }
    445         # check for "){"
    446         if($l =~ /^(.*)\)\{/) {
    447             checkwarn("PARENBRACE",
    448                       $line, length($1)+1, $file, $l, "missing space after close paren");
    449         }
    450 
    451         # check for space before the semicolon last in a line
    452         if($l =~ /^(.*[^ ].*) ;$/) {
    453             checkwarn("SPACESEMILCOLON",
    454                       $line, length($1), $file, $ol, "space before last semicolon");
    455         }
    456 
    457         # scan for use of banned functions
    458         if($l =~ /^(.*\W)
    459                    (gets|
    460 	            strtok|
    461                     v?sprintf|
    462                     (str|_mbs|_tcs|_wcs)n?cat|
    463                     LoadLibrary(Ex)?(A|W)?)
    464                    \s*\(
    465                  /x) {
    466             checkwarn("BANNEDFUNC",
    467                       $line, length($1), $file, $ol,
    468                       "use of $2 is banned");
    469         }
    470 
    471         # scan for use of non-binary fopen without the macro
    472         if($l =~ /^(.*\W)fopen\s*\([^,]*, *\"([^"]*)/) {
    473             my $mode = $2;
    474             if($mode !~ /b/) {
    475                 checkwarn("FOPENMODE",
    476                           $line, length($1), $file, $ol,
    477                           "use of non-binary fopen without FOPEN_* macro: $mode");
    478             }
    479         }
    480 
    481         # check for open brace first on line but not first column
    482         # only alert if previous line ended with a close paren and wasn't a cpp
    483         # line
    484         if((($prevl =~ /\)\z/) && ($prevl !~ /^ *#/)) && ($l =~ /^( +)\{/)) {
    485             checkwarn("BRACEPOS",
    486                       $line, length($1), $file, $ol, "badly placed open brace");
    487         }
    488 
    489         # if the previous line starts with if/while/for AND ends with an open
    490         # brace, check that this line is indented $indent more steps, if not
    491         # a cpp line
    492         if($prevl =~ /^( *)(if|while|for)\(.*\{\z/) {
    493             my $first = length($1);
    494 
    495             # this line has some character besides spaces
    496             if(($l !~ /^ *#/) && ($l =~ /^( *)[^ ]/)) {
    497                 my $second = length($1);
    498                 my $expect = $first+$indent;
    499                 if($expect != $second) {
    500                     my $diff = $second - $first;
    501                     checkwarn("INDENTATION", $line, length($1), $file, $ol,
    502                               "not indented $indent steps, uses $diff)");
    503 
    504                 }
    505             }
    506         }
    507 
    508         # check for 'char * name'
    509         if(($l =~ /(^.*(char|int|long|void|curl_slist|CURL|CURLM|CURLMsg|curl_httppost) *(\*+)) (\w+)/) && ($4 ne "const")) {
    510             checkwarn("ASTERISKNOSPACE",
    511                       $line, length($1), $file, $ol,
    512                       "no space after declarative asterisk");
    513         }
    514         # check for 'char*'
    515         if(($l =~ /(^.*(char|int|long|void|curl_slist|CURL|CURLM|CURLMsg|curl_httppost|sockaddr_in|FILE)\*)/)) {
    516             checkwarn("ASTERISKNOSPACE",
    517                       $line, length($1)-1, $file, $ol,
    518                       "no space before asterisk");
    519         }
    520 
    521         # check for 'void func() {', but avoid false positives by requiring
    522         # both an open and closed parentheses before the open brace
    523         if($l =~ /^((\w).*)\{\z/) {
    524             my $k = $1;
    525             $k =~ s/const *//;
    526             $k =~ s/static *//;
    527             if($k =~ /\(.*\)/) {
    528                 checkwarn("BRACEPOS",
    529                           $line, length($l)-1, $file, $ol,
    530                           "wrongly placed open brace");
    531             }
    532         }
    533 
    534         # check for equals sign without spaces next to it
    535         if($nostr =~ /(.*)\=[a-z0-9]/i) {
    536             checkwarn("EQUALSNOSPACE",
    537                       $line, length($1)+1, $file, $ol,
    538                       "no space after equals sign");
    539         }
    540         # check for equals sign without spaces before it
    541         elsif($nostr =~ /(.*)[a-z0-9]\=/i) {
    542             checkwarn("NOSPACEEQUALS",
    543                       $line, length($1)+1, $file, $ol,
    544                       "no space before equals sign");
    545         }
    546 
    547         # check for plus signs without spaces next to it
    548         if($nostr =~ /(.*)[^+]\+[a-z0-9]/i) {
    549             checkwarn("PLUSNOSPACE",
    550                       $line, length($1)+1, $file, $ol,
    551                       "no space after plus sign");
    552         }
    553         # check for plus sign without spaces before it
    554         elsif($nostr =~ /(.*)[a-z0-9]\+[^+]/i) {
    555             checkwarn("NOSPACEPLUS",
    556                       $line, length($1)+1, $file, $ol,
    557                       "no space before plus sign");
    558         }
    559 
    560         # check for semicolons without space next to it
    561         if($nostr =~ /(.*)\;[a-z0-9]/i) {
    562             checkwarn("SEMINOSPACE",
    563                       $line, length($1)+1, $file, $ol,
    564                       "no space after semilcolon");
    565         }
    566 
    567         # check for more than one consecutive space before open brace or
    568         # question mark. Skip lines containing strings since they make it hard
    569         # due to artificially getting multiple spaces
    570         if(($l eq $nostr) &&
    571            $nostr =~ /^(.*(\S)) + [{?]/i) {
    572             checkwarn("MULTISPACE",
    573                       $line, length($1)+1, $file, $ol,
    574                       "multiple space");
    575             print STDERR "L: $l\n";
    576             print STDERR "nostr: $nostr\n";
    577         }
    578 
    579         $line++;
    580         $prevl = $ol;
    581     }
    582 
    583     if(!$copyright) {
    584         checkwarn("COPYRIGHT", 1, 0, $file, "", "Missing copyright statement", 1);
    585     }
    586     if($incomment) {
    587         checkwarn("OPENCOMMENT", 1, 0, $file, "", "Missing closing comment", 1);
    588     }
    589 
    590     checksrc_endoffile($file);
    591 
    592     close(R);
    593 
    594 }
    595 
    596 
    597 if($errors || $warnings || $verbose) {
    598     printf "checksrc: %d errors and %d warnings\n", $errors, $warnings;
    599     if($supressed) {
    600         printf "checksrc: %d errors and %d warnings suppressed\n",
    601         $serrors,
    602         $swarnings;
    603     }
    604     exit 5; # return failure
    605 }
    606