1#!/usr/bin/env perl
2#***************************************************************************
3#                                  _   _ ____  _
4#  Project                     ___| | | |  _ \| |
5#                             / __| | | | |_) | |
6#                            | (__| |_| |  _ <| |___
7#                             \___|\___/|_| \_\_____|
8#
9# Copyright (C) 2011 - 2020, 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.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
24use strict;
25use warnings;
26
27my $max_column = 79;
28my $indent = 2;
29
30my $warnings = 0;
31my $swarnings = 0;
32my $errors = 0;
33my $serrors = 0;
34my $suppressed; # skipped problems
35my $file;
36my $dir=".";
37my $wlist="";
38my @alist;
39my $windows_os = $^O eq 'MSWin32' || $^O eq 'cygwin' || $^O eq 'msys';
40my $verbose;
41my %skiplist;
42
43my %ignore;
44my %ignore_set;
45my %ignore_used;
46my @ignore_line;
47
48my %warnings_extended = (
49    'COPYRIGHTYEAR'    => 'copyright year incorrect',
50    );
51
52my %warnings = (
53    'LONGLINE'         => "Line longer than $max_column",
54    'TABS'             => 'TAB characters not allowed',
55    'TRAILINGSPACE'    => 'Trailing whitespace on the line',
56    'CPPCOMMENTS'      => '// comment detected',
57    'SPACEBEFOREPAREN' => 'space before an open parenthesis',
58    'SPACEAFTERPAREN'  => 'space after open parenthesis',
59    'SPACEBEFORECLOSE' => 'space before a close parenthesis',
60    'SPACEBEFORECOMMA' => 'space before a comma',
61    'RETURNNOSPACE'    => 'return without space',
62    'COMMANOSPACE'     => 'comma without following space',
63    'BRACEELSE'        => '} else on the same line',
64    'PARENBRACE'       => '){ without sufficient space',
65    'SPACESEMICOLON'   => 'space before semicolon',
66    'BANNEDFUNC'       => 'a banned function was used',
67    'FOPENMODE'        => 'fopen needs a macro for the mode string',
68    'BRACEPOS'         => 'wrong position for an open brace',
69    'INDENTATION'      => 'wrong start column for code',
70    'COPYRIGHT'        => 'file missing a copyright statement',
71    'BADCOMMAND'       => 'bad !checksrc! instruction',
72    'UNUSEDIGNORE'     => 'a warning ignore was not used',
73    'OPENCOMMENT'      => 'file ended with a /* comment still "open"',
74    'ASTERISKSPACE'    => 'pointer declared with space after asterisk',
75    'ASTERISKNOSPACE'  => 'pointer declared without space before asterisk',
76    'ASSIGNWITHINCONDITION' => 'assignment within conditional expression',
77    'EQUALSNOSPACE'    => 'equals sign without following space',
78    'NOSPACEEQUALS'    => 'equals sign without preceding space',
79    'SEMINOSPACE'      => 'semicolon without following space',
80    'MULTISPACE'       => 'multiple spaces used when not suitable',
81    'SIZEOFNOPAREN'    => 'use of sizeof without parentheses',
82    'SNPRINTF'         => 'use of snprintf',
83    'ONELINECONDITION' => 'conditional block on the same line as the if()',
84    'TYPEDEFSTRUCT'    => 'typedefed struct',
85    'DOBRACE'          => 'A single space between do and open brace',
86    'BRACEWHILE'       => 'A single space between open brace and while',
87    'EXCLAMATIONSPACE' => 'Whitespace after exclamation mark in expression',
88    'EMPTYLINEBRACE'   => 'Empty line before the open brace',
89    );
90
91sub readskiplist {
92    open(W, "<$dir/checksrc.skip") or return;
93    my @all=<W>;
94    for(@all) {
95        $windows_os ? $_ =~ s/\r?\n$// : chomp;
96        $skiplist{$_}=1;
97    }
98    close(W);
99}
100
101# Reads the .checksrc in $dir for any extended warnings to enable locally.
102# Currently there is no support for disabling warnings from the standard set,
103# and since that's already handled via !checksrc! commands there is probably
104# little use to add it.
105sub readlocalfile {
106    my $i = 0;
107
108    open(my $rcfile, "<", "$dir/.checksrc") or return;
109
110    while(<$rcfile>) {
111        $i++;
112
113        # Lines starting with '#' are considered comments
114        if (/^\s*(#.*)/) {
115            next;
116        }
117        elsif (/^\s*enable ([A-Z]+)$/) {
118            if(!defined($warnings_extended{$1})) {
119                print STDERR "invalid warning specified in .checksrc: \"$1\"\n";
120                next;
121            }
122            $warnings{$1} = $warnings_extended{$1};
123        }
124        elsif (/^\s*disable ([A-Z]+)$/) {
125            if(!defined($warnings{$1})) {
126                print STDERR "invalid warning specified in .checksrc: \"$1\"\n";
127                next;
128            }
129            # Accept-list
130            push @alist, $1;
131        }
132        else {
133            die "Invalid format in $dir/.checksrc on line $i\n";
134        }
135    }
136    close($rcfile);
137}
138
139sub checkwarn {
140    my ($name, $num, $col, $file, $line, $msg, $error) = @_;
141
142    my $w=$error?"error":"warning";
143    my $nowarn=0;
144
145    #if(!$warnings{$name}) {
146    #    print STDERR "Dev! there's no description for $name!\n";
147    #}
148
149    # checksrc.skip
150    if($skiplist{$line}) {
151        $nowarn = 1;
152    }
153    # !checksrc! controlled
154    elsif($ignore{$name}) {
155        $ignore{$name}--;
156        $ignore_used{$name}++;
157        $nowarn = 1;
158        if(!$ignore{$name}) {
159            # reached zero, enable again
160            enable_warn($name, $num, $file, $line);
161        }
162    }
163
164    if($nowarn) {
165        $suppressed++;
166        if($w) {
167            $swarnings++;
168        }
169        else {
170            $serrors++;
171        }
172        return;
173    }
174
175    if($w) {
176        $warnings++;
177    }
178    else {
179        $errors++;
180    }
181
182    $col++;
183    print "$file:$num:$col: $w: $msg ($name)\n";
184    print " $line\n";
185
186    if($col < 80) {
187        my $pref = (' ' x $col);
188        print "${pref}^\n";
189    }
190}
191
192$file = shift @ARGV;
193
194while(defined $file) {
195
196    if($file =~ /-D(.*)/) {
197        $dir = $1;
198        $file = shift @ARGV;
199        next;
200    }
201    elsif($file =~ /-W(.*)/) {
202        $wlist .= " $1 ";
203        $file = shift @ARGV;
204        next;
205    }
206    elsif($file =~ /-A(.+)/) {
207        push @alist, $1;
208        $file = shift @ARGV;
209        next;
210    }
211    elsif($file =~ /-i([1-9])/) {
212        $indent = $1 + 0;
213        $file = shift @ARGV;
214        next;
215    }
216    elsif($file =~ /-m([0-9]+)/) {
217        $max_column = $1 + 0;
218        $file = shift @ARGV;
219        next;
220    }
221    elsif($file =~ /^(-h|--help)/) {
222        undef $file;
223        last;
224    }
225
226    last;
227}
228
229if(!$file) {
230    print "checksrc.pl [option] <file1> [file2] ...\n";
231    print " Options:\n";
232    print "  -A[rule]  Accept this violation, can be used multiple times\n";
233    print "  -D[DIR]   Directory to prepend file names\n";
234    print "  -h        Show help output\n";
235    print "  -W[file]  Skip the given file - ignore all its flaws\n";
236    print "  -i<n>     Indent spaces. Default: 2\n";
237    print "  -m<n>     Maximum line length. Default: 79\n";
238    print "\nDetects and warns for these problems:\n";
239    for(sort keys %warnings) {
240        printf (" %-18s: %s\n", $_, $warnings{$_});
241    }
242    exit;
243}
244
245readskiplist();
246readlocalfile();
247
248do {
249    if("$wlist" !~ / $file /) {
250        my $fullname = $file;
251        $fullname = "$dir/$file" if ($fullname !~ '^\.?\.?/');
252        scanfile($fullname);
253    }
254    $file = shift @ARGV;
255
256} while($file);
257
258sub accept_violations {
259    for my $r (@alist) {
260        if(!$warnings{$r}) {
261            print "'$r' is not a warning to accept!\n";
262            exit;
263        }
264        $ignore{$r}=999999;
265        $ignore_used{$r}=0;
266    }
267}
268
269sub checksrc_clear {
270    undef %ignore;
271    undef %ignore_set;
272    undef @ignore_line;
273}
274
275sub checksrc_endoffile {
276    my ($file) = @_;
277    for(keys %ignore_set) {
278        if($ignore_set{$_} && !$ignore_used{$_}) {
279            checkwarn("UNUSEDIGNORE", $ignore_set{$_},
280                      length($_)+11, $file,
281                      $ignore_line[$ignore_set{$_}],
282                      "Unused ignore: $_");
283        }
284    }
285}
286
287sub enable_warn {
288    my ($what, $line, $file, $l) = @_;
289
290    # switch it back on, but warn if not triggered!
291    if(!$ignore_used{$what}) {
292        checkwarn("UNUSEDIGNORE",
293                  $line, length($what) + 11, $file, $l,
294                  "No warning was inhibited!");
295    }
296    $ignore_set{$what}=0;
297    $ignore_used{$what}=0;
298    $ignore{$what}=0;
299}
300sub checksrc {
301    my ($cmd, $line, $file, $l) = @_;
302    if($cmd =~ / *([^ ]*) *(.*)/) {
303        my ($enable, $what) = ($1, $2);
304        $what =~ s: *\*/$::; # cut off end of C comment
305        # print "ENABLE $enable WHAT $what\n";
306        if($enable eq "disable") {
307            my ($warn, $scope)=($1, $2);
308            if($what =~ /([^ ]*) +(.*)/) {
309                ($warn, $scope)=($1, $2);
310            }
311            else {
312                $warn = $what;
313                $scope = 1;
314            }
315            # print "IGNORE $warn for SCOPE $scope\n";
316            if($scope eq "all") {
317                $scope=999999;
318            }
319
320            # Comparing for a literal zero rather than the scalar value zero
321            # covers the case where $scope contains the ending '*' from the
322            # comment. If we use a scalar comparison (==) we induce warnings
323            # on non-scalar contents.
324            if($scope eq "0") {
325                checkwarn("BADCOMMAND",
326                          $line, 0, $file, $l,
327                          "Disable zero not supported, did you mean to enable?");
328            }
329            elsif($ignore_set{$warn}) {
330                checkwarn("BADCOMMAND",
331                          $line, 0, $file, $l,
332                          "$warn already disabled from line $ignore_set{$warn}");
333            }
334            else {
335                $ignore{$warn}=$scope;
336                $ignore_set{$warn}=$line;
337                $ignore_line[$line]=$l;
338            }
339        }
340        elsif($enable eq "enable") {
341            enable_warn($what, $line, $file, $l);
342        }
343        else {
344            checkwarn("BADCOMMAND",
345                      $line, 0, $file, $l,
346                      "Illegal !checksrc! command");
347        }
348    }
349}
350
351sub nostrings {
352    my ($str) = @_;
353    $str =~ s/\".*\"//g;
354    return $str;
355}
356
357sub scanfile {
358    my ($file) = @_;
359
360    my $line = 1;
361    my $prevl="";
362    my $l;
363    open(R, "<$file") || die "failed to open $file";
364
365    my $incomment=0;
366    my @copyright=();
367    checksrc_clear(); # for file based ignores
368    accept_violations();
369
370    while(<R>) {
371        $windows_os ? $_ =~ s/\r?\n$// : chomp;
372        my $l = $_;
373        my $ol = $l; # keep the unmodified line for error reporting
374        my $column = 0;
375
376        # check for !checksrc! commands
377        if($l =~ /\!checksrc\! (.*)/) {
378            my $cmd = $1;
379            checksrc($cmd, $line, $file, $l)
380        }
381
382        # check for a copyright statement and save the years
383        if($l =~ /\* +copyright .* \d\d\d\d/i) {
384            while($l =~ /([\d]{4})/g) {
385                push @copyright, {
386                  year => $1,
387                  line => $line,
388                  col => index($l, $1),
389                  code => $l
390                };
391            }
392        }
393
394        # detect long lines
395        if(length($l) > $max_column) {
396            checkwarn("LONGLINE", $line, length($l), $file, $l,
397                      "Longer than $max_column columns");
398        }
399        # detect TAB characters
400        if($l =~ /^(.*)\t/) {
401            checkwarn("TABS",
402                      $line, length($1), $file, $l, "Contains TAB character", 1);
403        }
404        # detect trailing whitespace
405        if($l =~ /^(.*)[ \t]+\z/) {
406            checkwarn("TRAILINGSPACE",
407                      $line, length($1), $file, $l, "Trailing whitespace");
408        }
409
410        # ------------------------------------------------------------
411        # Above this marker, the checks were done on lines *including*
412        # comments
413        # ------------------------------------------------------------
414
415        # strip off C89 comments
416
417      comment:
418        if(!$incomment) {
419            if($l =~ s/\/\*.*\*\// /g) {
420                # full /* comments */ were removed!
421            }
422            if($l =~ s/\/\*.*//) {
423                # start of /* comment was removed
424                $incomment = 1;
425            }
426        }
427        else {
428            if($l =~ s/.*\*\///) {
429                # end of comment */ was removed
430                $incomment = 0;
431                goto comment;
432            }
433            else {
434                # still within a comment
435                $l="";
436            }
437        }
438
439        # ------------------------------------------------------------
440        # Below this marker, the checks were done on lines *without*
441        # comments
442        # ------------------------------------------------------------
443
444        # crude attempt to detect // comments without too many false
445        # positives
446        if($l =~ /^(([^"\*]*)[^:"]|)\/\//) {
447            checkwarn("CPPCOMMENTS",
448                      $line, length($1), $file, $l, "\/\/ comment");
449        }
450
451        my $nostr = nostrings($l);
452        # check spaces after for/if/while/function call
453        if($nostr =~ /^(.*)(for|if|while| ([a-zA-Z0-9_]+)) \((.)/) {
454            if($1 =~ / *\#/) {
455                # this is a #if, treat it differently
456            }
457            elsif(defined $3 && $3 eq "return") {
458                # return must have a space
459            }
460            elsif(defined $3 && $3 eq "case") {
461                # case must have a space
462            }
463            elsif($4 eq "*") {
464                # (* beginning makes the space OK!
465            }
466            elsif($1 =~ / *typedef/) {
467                # typedefs can use space-paren
468            }
469            else {
470                checkwarn("SPACEBEFOREPAREN", $line, length($1)+length($2), $file, $l,
471                          "$2 with space");
472            }
473        }
474
475        # check spaces in 'do {'
476        if($nostr =~ /^( *)do( *)\{/ && length($2) != 1) {
477            checkwarn("DOBRACE", $line, length($1) + 2, $file, $l, "one space after do before brace");
478        }
479        # check spaces in 'do {'
480        elsif($nostr =~ /^( *)\}( *)while/ && length($2) != 1) {
481            checkwarn("BRACEWHILE", $line, length($1) + 2, $file, $l, "one space between brace and while");
482        }
483        if($nostr =~ /^((.*\s)(if) *\()(.*)\)(.*)/) {
484            my $pos = length($1);
485            my $postparen = $5;
486            my $cond = $4;
487            if($cond =~ / = /) {
488                checkwarn("ASSIGNWITHINCONDITION",
489                          $line, $pos+1, $file, $l,
490                          "assignment within conditional expression");
491            }
492            my $temp = $cond;
493            $temp =~ s/\(//g; # remove open parens
494            my $openc = length($cond) - length($temp);
495
496            $temp = $cond;
497            $temp =~ s/\)//g; # remove close parens
498            my $closec = length($cond) - length($temp);
499            my $even = $openc == $closec;
500
501            if($l =~ / *\#/) {
502                # this is a #if, treat it differently
503            }
504            elsif($even && $postparen &&
505               ($postparen !~ /^ *$/) && ($postparen !~ /^ *[,{&|\\]+/)) {
506                print STDERR "5: '$postparen'\n";
507                checkwarn("ONELINECONDITION",
508                          $line, length($l)-length($postparen), $file, $l,
509                          "conditional block on the same line");
510            }
511        }
512        # check spaces after open parentheses
513        if($l =~ /^(.*[a-z])\( /i) {
514            checkwarn("SPACEAFTERPAREN",
515                      $line, length($1)+1, $file, $l,
516                      "space after open parenthesis");
517        }
518
519        # check spaces before close parentheses, unless it was a space or a
520        # close parenthesis!
521        if($l =~ /(.*[^\) ]) \)/) {
522            checkwarn("SPACEBEFORECLOSE",
523                      $line, length($1)+1, $file, $l,
524                      "space before close parenthesis");
525        }
526
527        # check spaces before comma!
528        if($l =~ /(.*[^ ]) ,/) {
529            checkwarn("SPACEBEFORECOMMA",
530                      $line, length($1)+1, $file, $l,
531                      "space before comma");
532        }
533
534        # check for "return(" without space
535        if($l =~ /^(.*)return\(/) {
536            if($1 =~ / *\#/) {
537                # this is a #if, treat it differently
538            }
539            else {
540                checkwarn("RETURNNOSPACE", $line, length($1)+6, $file, $l,
541                          "return without space before paren");
542            }
543        }
544
545        # check for "sizeof" without parenthesis
546        if(($l =~ /^(.*)sizeof *([ (])/) && ($2 ne "(")) {
547            if($1 =~ / *\#/) {
548                # this is a #if, treat it differently
549            }
550            else {
551                checkwarn("SIZEOFNOPAREN", $line, length($1)+6, $file, $l,
552                          "sizeof without parenthesis");
553            }
554        }
555
556        # check for comma without space
557        if($l =~ /^(.*),[^ \n]/) {
558            my $pref=$1;
559            my $ign=0;
560            if($pref =~ / *\#/) {
561                # this is a #if, treat it differently
562                $ign=1;
563            }
564            elsif($pref =~ /\/\*/) {
565                # this is a comment
566                $ign=1;
567            }
568            elsif($pref =~ /[\"\']/) {
569                $ign = 1;
570                # There is a quote here, figure out whether the comma is
571                # within a string or '' or not.
572                if($pref =~ /\"/) {
573                    # within a string
574                }
575                elsif($pref =~ /\'$/) {
576                    # a single letter
577                }
578                else {
579                    $ign = 0;
580                }
581            }
582            if(!$ign) {
583                checkwarn("COMMANOSPACE", $line, length($pref)+1, $file, $l,
584                          "comma without following space");
585            }
586        }
587
588        # check for "} else"
589        if($l =~ /^(.*)\} *else/) {
590            checkwarn("BRACEELSE",
591                      $line, length($1), $file, $l, "else after closing brace on same line");
592        }
593        # check for "){"
594        if($l =~ /^(.*)\)\{/) {
595            checkwarn("PARENBRACE",
596                      $line, length($1)+1, $file, $l, "missing space after close paren");
597        }
598        # check for "^{" with an empty line before it
599        if(($l =~ /^\{/) && ($prevl =~ /^[ \t]*\z/)) {
600            checkwarn("EMPTYLINEBRACE",
601                      $line, 0, $file, $l, "empty line before open brace");
602        }
603
604        # check for space before the semicolon last in a line
605        if($l =~ /^(.*[^ ].*) ;$/) {
606            checkwarn("SPACESEMICOLON",
607                      $line, length($1), $file, $ol, "space before last semicolon");
608        }
609
610        # scan for use of banned functions
611        if($l =~ /^(.*\W)
612                   (gmtime|localtime|
613                    gets|
614                    strtok|
615                    v?sprintf|
616                    (str|_mbs|_tcs|_wcs)n?cat|
617                    LoadLibrary(Ex)?(A|W)?)
618                   \s*\(
619                 /x) {
620            checkwarn("BANNEDFUNC",
621                      $line, length($1), $file, $ol,
622                      "use of $2 is banned");
623        }
624
625        # scan for use of snprintf for curl-internals reasons
626        if($l =~ /^(.*\W)(v?snprintf)\s*\(/x) {
627            checkwarn("SNPRINTF",
628                      $line, length($1), $file, $ol,
629                      "use of $2 is banned");
630        }
631
632        # scan for use of non-binary fopen without the macro
633        if($l =~ /^(.*\W)fopen\s*\([^,]*, *\"([^"]*)/) {
634            my $mode = $2;
635            if($mode !~ /b/) {
636                checkwarn("FOPENMODE",
637                          $line, length($1), $file, $ol,
638                          "use of non-binary fopen without FOPEN_* macro: $mode");
639            }
640        }
641
642        # check for open brace first on line but not first column
643        # only alert if previous line ended with a close paren and wasn't a cpp
644        # line
645        if((($prevl =~ /\)\z/) && ($prevl !~ /^ *#/)) && ($l =~ /^( +)\{/)) {
646            checkwarn("BRACEPOS",
647                      $line, length($1), $file, $ol, "badly placed open brace");
648        }
649
650        # if the previous line starts with if/while/for AND ends with an open
651        # brace, or an else statement, check that this line is indented $indent
652        # more steps, if not a cpp line
653        if($prevl =~ /^( *)((if|while|for)\(.*\{|else)\z/) {
654            my $first = length($1);
655
656            # this line has some character besides spaces
657            if(($l !~ /^ *#/) && ($l =~ /^( *)[^ ]/)) {
658                my $second = length($1);
659                my $expect = $first+$indent;
660                if($expect != $second) {
661                    my $diff = $second - $first;
662                    checkwarn("INDENTATION", $line, length($1), $file, $ol,
663                              "not indented $indent steps (uses $diff)");
664
665                }
666            }
667        }
668
669        # check for 'char * name'
670        if(($l =~ /(^.*(char|int|long|void|CURL|CURLM|CURLMsg|[cC]url_[A-Za-z_]+|struct [a-zA-Z_]+) *(\*+)) (\w+)/) && ($4 !~ /^(const|volatile)$/)) {
671            checkwarn("ASTERISKSPACE",
672                      $line, length($1), $file, $ol,
673                      "space after declarative asterisk");
674        }
675        # check for 'char*'
676        if(($l =~ /(^.*(char|int|long|void|curl_slist|CURL|CURLM|CURLMsg|curl_httppost|sockaddr_in|FILE)\*)/)) {
677            checkwarn("ASTERISKNOSPACE",
678                      $line, length($1)-1, $file, $ol,
679                      "no space before asterisk");
680        }
681
682        # check for 'void func() {', but avoid false positives by requiring
683        # both an open and closed parentheses before the open brace
684        if($l =~ /^((\w).*)\{\z/) {
685            my $k = $1;
686            $k =~ s/const *//;
687            $k =~ s/static *//;
688            if($k =~ /\(.*\)/) {
689                checkwarn("BRACEPOS",
690                          $line, length($l)-1, $file, $ol,
691                          "wrongly placed open brace");
692            }
693        }
694
695        # check for equals sign without spaces next to it
696        if($nostr =~ /(.*)\=[a-z0-9]/i) {
697            checkwarn("EQUALSNOSPACE",
698                      $line, length($1)+1, $file, $ol,
699                      "no space after equals sign");
700        }
701        # check for equals sign without spaces before it
702        elsif($nostr =~ /(.*)[a-z0-9]\=/i) {
703            checkwarn("NOSPACEEQUALS",
704                      $line, length($1)+1, $file, $ol,
705                      "no space before equals sign");
706        }
707
708        # check for plus signs without spaces next to it
709        if($nostr =~ /(.*)[^+]\+[a-z0-9]/i) {
710            checkwarn("PLUSNOSPACE",
711                      $line, length($1)+1, $file, $ol,
712                      "no space after plus sign");
713        }
714        # check for plus sign without spaces before it
715        elsif($nostr =~ /(.*)[a-z0-9]\+[^+]/i) {
716            checkwarn("NOSPACEPLUS",
717                      $line, length($1)+1, $file, $ol,
718                      "no space before plus sign");
719        }
720
721        # check for semicolons without space next to it
722        if($nostr =~ /(.*)\;[a-z0-9]/i) {
723            checkwarn("SEMINOSPACE",
724                      $line, length($1)+1, $file, $ol,
725                      "no space after semicolon");
726        }
727
728        # typedef struct ... {
729        if($nostr =~ /^(.*)typedef struct.*{/) {
730            checkwarn("TYPEDEFSTRUCT",
731                      $line, length($1)+1, $file, $ol,
732                      "typedef'ed struct");
733        }
734
735        if($nostr =~ /(.*)! +(\w|\()/) {
736            checkwarn("EXCLAMATIONSPACE",
737                      $line, length($1)+1, $file, $ol,
738                      "space after exclamation mark");
739        }
740
741        # check for more than one consecutive space before open brace or
742        # question mark. Skip lines containing strings since they make it hard
743        # due to artificially getting multiple spaces
744        if(($l eq $nostr) &&
745           $nostr =~ /^(.*(\S)) + [{?]/i) {
746            checkwarn("MULTISPACE",
747                      $line, length($1)+1, $file, $ol,
748                      "multiple space");
749            print STDERR "L: $l\n";
750            print STDERR "nostr: $nostr\n";
751        }
752
753        $line++;
754        $prevl = $ol;
755    }
756
757    if(!scalar(@copyright)) {
758        checkwarn("COPYRIGHT", 1, 0, $file, "", "Missing copyright statement", 1);
759    }
760
761    # COPYRIGHTYEAR is a extended warning so we must first see if it has been
762    # enabled in .checksrc
763    if(defined($warnings{"COPYRIGHTYEAR"})) {
764        # The check for updated copyrightyear is overly complicated in order to
765        # not punish current hacking for past sins. The copyright years are
766        # right now a bit behind, so enforcing copyright year checking on all
767        # files would cause hundreds of errors. Instead we only look at files
768        # which are tracked in the Git repo and edited in the workdir, or
769        # committed locally on the branch without being in upstream master.
770        #
771        # The simple and naive test is to simply check for the current year,
772        # but updating the year even without an edit is against project policy
773        # (and it would fail every file on January 1st).
774        #
775        # A rather more interesting, and correct, check would be to not test
776        # only locally committed files but inspect all files wrt the year of
777        # their last commit. Removing the `git rev-list origin/master..HEAD`
778        # condition below will enfore copyright year checks against the year
779        # the file was last committed (and thus edited to some degree).
780        my $commityear = undef;
781        @copyright = sort {$$b{year} cmp $$a{year}} @copyright;
782
783        # if the file is modified, assume commit year this year
784        if(`git status -s -- $file` =~ /^ [MARCU]/) {
785            $commityear = (localtime(time))[5] + 1900;
786        }
787        else {
788            # min-parents=1 to ignore wrong initial commit in truncated repos
789            my $grl = `git rev-list --max-count=1 --min-parents=1 --timestamp HEAD -- $file`;
790            if($grl) {
791                chomp $grl;
792                $commityear = (localtime((split(/ /, $grl))[0]))[5] + 1900;
793            }
794        }
795
796        if(defined($commityear) && scalar(@copyright) &&
797           $copyright[0]{year} != $commityear) {
798            checkwarn("COPYRIGHTYEAR", $copyright[0]{line}, $copyright[0]{col},
799                      $file, $copyright[0]{code},
800                      "Copyright year out of date, should be $commityear, " .
801                      "is $copyright[0]{year}", 1);
802        }
803    }
804
805    if($incomment) {
806        checkwarn("OPENCOMMENT", 1, 0, $file, "", "Missing closing comment", 1);
807    }
808
809    checksrc_endoffile($file);
810
811    close(R);
812
813}
814
815
816if($errors || $warnings || $verbose) {
817    printf "checksrc: %d errors and %d warnings\n", $errors, $warnings;
818    if($suppressed) {
819        printf "checksrc: %d errors and %d warnings suppressed\n",
820        $serrors,
821        $swarnings;
822    }
823    exit 5; # return failure
824}
825