1#!/usr/bin/env perl
2#***************************************************************************
3#                                  _   _ ____  _
4#  Project                     ___| | | |  _ \| |
5#                             / __| | | | |_) | |
6#                            | (__| |_| |  _ <| |___
7#                             \___|\___/|_| \_\_____|
8#
9# Copyright (C) 2011 - 2017, Daniel Stenberg, <daniel@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
24my $max_column = 79;
25my $indent = 2;
26
27my $warnings;
28my $errors;
29my $supressed; # whitelisted problems
30my $file;
31my $dir=".";
32my $wlist;
33my $windows_os = $^O eq 'MSWin32' || $^O eq 'msys' || $^O eq 'cygwin';
34my $verbose;
35my %whitelist;
36
37my %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
68sub 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
78sub 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
133while(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
153if(!$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
166readwhitelist();
167
168do {
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
178sub checksrc_clear {
179    undef %ignore;
180    undef %ignore_set;
181    undef @ignore_line;
182}
183
184sub 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
196sub 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}
209sub 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
251sub nostrings {
252    my ($str) = @_;
253    $str =~ s/\".*\"//g;
254    return $str;
255}
256
257sub 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
597if($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