Enable the record layer to call the ssl_security callback
[openssl.git] / util / check-format.pl
index 0619240f828b3979ca0316d04e76314f9c6f4bdf..be84d733ff2f6eff289446ffe751f8cbb772fbf0 100755 (executable)
@@ -1,7 +1,7 @@
-#!/usr/bin/perl
+#! /usr/bin/env perl
 #
-# Copyright 2020 The OpenSSL Project Authors. All Rights Reserved.
-# Copyright Siemens AG 2019-2020
+# Copyright 2020-2022 The OpenSSL Project Authors. All Rights Reserved.
+# Copyright Siemens AG 2019-2022
 #
 # Licensed under the Apache License 2.0 (the "License").
 # You may not use this file except in compliance with the License.
 #
 # usage:
 #   check-format.pl [-l|--sloppy-len] [-l|--sloppy-bodylen]
-#                   [-s|--sloppy-spc] [-c|--sloppy-cmt] [-m|--sloppy-macro]
-#                   [-h|--sloppy-hang] [-1|--1-stmt]
+#                   [-s|--sloppy-space] [-c|--sloppy-comment]
+#                   [-m|--sloppy-macro] [-h|--sloppy-hang]
+#                   [-e|--eol-comment] [-1|--1-stmt]
 #                   <files>
 #
+# run self-tests:
+#   util/check-format.pl util/check-format-test-positives.c
+#   util/check-format.pl util/check-format-test-negatives.c
+#
 # checks adherence to the formatting rules of the OpenSSL coding guidelines
 # assuming that the input files contain syntactically correct C code.
 # This pragmatic tool is incomplete and yields some false positives.
 # Still it should be useful for detecting most typical glitches.
 #
 # options:
-#  -l | --sloppy-len   increase accepted max line length from 80 to 84
+#  -l | --sloppy-len     increase accepted max line length from 80 to 84
 #  -l | --sloppy-bodylen do not report function body length > 200
-#  -s | --sloppy-spc   do not report whitespace nits
-#  -c | --sloppy-cmt   do not report indentation of comments
-#                      Otherwise for each multi-line comment the indentation of
-#                      its lines is checked for consistency. For each comment
-#                      that does not begin to the right of normal code its
-#                      indentation must be as for normal code, while in case it
-#                      also has no normal code to its right it is considered to
-#                      refer to the following line and may be indented equally.
-#  -m | --sloppy-macro allow missing extra indentation of macro bodies
-#  -h | --sloppy-hang  when checking hanging indentation, do not report
-#                      * same indentation as on line before
-#                      * same indentation as non-hanging indent level
-#                      * indentation moved left (not beyond non-hanging indent)
-#                        just to fit contents within the line length limit
-#  -1 | --1-stmt       do more aggressive checks for { 1 stmt } - see below
+#  -s | --sloppy-space   do not report whitespace nits
+#  -c | --sloppy-comment do not report indentation of comments
+#                        Otherwise for each multi-line comment the indentation of
+#                        its lines is checked for consistency. For each comment
+#                        that does not begin to the right of normal code its
+#                        indentation must be as for normal code, while in case it
+#                        also has no normal code to its right it is considered to
+#                        refer to the following line and may be indented equally.
+#  -m | --sloppy-macro   allow missing extra indentation of macro bodies
+#  -h | --sloppy-hang    when checking hanging indentation, do not report
+#                        * same indentation as on line before
+#                        * same indentation as non-hanging indent level
+#                        * indentation moved left (not beyond non-hanging indent)
+#                          just to fit contents within the line length limit
+#  -e | --eol-comment    report needless intermediate multiple consecutive spaces also before end-of-line comments
+#  -1 | --1-stmt         do more aggressive checks for { 1 stmt } - see below
 #
 # There are non-trivial false positives and negatives such as the following.
 #
 #   except within if ... else constructs where some branch contains more than one
 #   statement. Since the exception is hard to recognize when such branches occur
 #   after the current position (such that false positives would be reported)
-#   the tool by checks for this rule by defaul only for do/while/for bodies.
+#   the tool by checks for this rule by default only for do/while/for bodies.
 #   Yet with the --1-stmt option false positives are preferred over negatives.
-#   False negatives occur if the braces are more than two non-empty lines apart.
+#   False negatives occur if the braces are more than two non-blank lines apart.
 #
-# * Use of multiple consecutive spaces is regarded a coding style nit except
-#   when done in order to align certain columns over multiple lines, e.g.:
+# * The presence of multiple consecutive spaces is regarded a coding style nit
+#   except when this is before end-of-line comments (unless the --eol-comment is given) and
+#   except when done in order to align certain columns over multiple lines, e.g.:
 #   # define AB  1
 #   # define CDE 22
 #   # define F   3333
-#   This pattern is recognized - and consequently double space not reported -
-#   for a given line if in the nonempty line before or after (if existing)
+#   This pattern is recognized - and consequently extra space not reported -
+#   for a given line if in the non-blank line before or after (if existing)
 #   for each occurrence of "  \S" (where \S means non-space) in the given line
 #   there is " \S" in the other line in the respective column position.
 #   This may lead to both false negatives (in case of coincidental " \S")
@@ -98,6 +105,7 @@ my $sloppy_SPC = 0;
 my $sloppy_hang = 0;
 my $sloppy_cmt = 0;
 my $sloppy_macro = 0;
+my $eol_cmt = 0;
 my $extended_1_stmt = 0;
 
 while ($ARGV[0] =~ m/^-(\w|-[\w\-]+)$/) {
@@ -106,14 +114,16 @@ while ($ARGV[0] =~ m/^-(\w|-[\w\-]+)$/) {
         $max_length += INDENT_LEVEL;
     } elsif ($arg =~ m/^(b|-sloppy-bodylen)$/) {
         $sloppy_bodylen = 1;
-    } elsif ($arg =~ m/^(s|-sloppy-spc)$/) {
-        $sloppy_SPC = 1;
-    } elsif ($arg =~ m/^(c|-sloppy-cmt)$/) {
+    } elsif ($arg =~ m/^(s|-sloppy-space)$/) {
+        $sloppy_SPC= 1;
+    } elsif ($arg =~ m/^(c|-sloppy-comment)$/) {
         $sloppy_cmt = 1;
     } elsif ($arg =~ m/^(m|-sloppy-macro)$/) {
         $sloppy_macro = 1;
     } elsif ($arg =~ m/^(h|-sloppy-hang)$/) {
         $sloppy_hang = 1;
+    } elsif ($arg =~ m/^(e|-eol-comment)$/) {
+        $eol_cmt = 1;
     } elsif ($arg =~ m/^(1|-1-stmt)$/) {
         $extended_1_stmt = 1;
     } else {
@@ -124,12 +134,14 @@ while ($ARGV[0] =~ m/^-(\w|-[\w\-]+)$/) {
 # status variables
 my $self_test;             # whether the current input file is regarded to contain (positive/negative) self-tests
 my $line;                  # current line number
-my $line_before;           # number of previous not essentially empty line (containing at most whitespace and '\')
-my $line_before2;          # number of not essentially empty line before previous not essentially empty line
-my $contents;              # contents of current line
-my $contents_before;       # contents of $line_before, if $line_before > 0
+my $line_before;           # number of previous not essentially blank line (containing at most whitespace and '\')
+my $line_before2;          # number of not essentially blank line before previous not essentially blank line
+my $contents;              # contents of current line (without blinding)
+#  $_                      # current line, where comments etc. get blinded
+my $code_contents_before;  # contents of previous non-comment non-directive line (without blinding), initially ""
+my $contents_before;       # contents of $line_before (without blinding), if $line_before > 0
 my $contents_before_;      # contents of $line_before after blinding comments etc., if $line_before > 0
-my $contents_before2;      # contents of $line_before2, if $line_before2 > 0
+my $contents_before2;      # contents of $line_before2  (without blinding), if $line_before2 > 0
 my $contents_before_2;     # contents of $line_before2 after blinding comments etc., if $line_before2 > 0
 my $in_multiline_string;   # line starts within multi-line string literal
 my $count;                 # -1 or number of leading whitespace characters (except newline) in current line,
@@ -157,6 +169,7 @@ my @nested_symbols;        # stack of hanging symbols '(', '{', '[', or '?', in
 my @nested_conds_indents;  # stack of hanging indents due to conditionals ('?' ... ':')
 my $expr_indent;           # resulting hanging indent within (multi-line) expressions including type exprs, else 0
 my $hanging_symbol;        # character ('(', '{', '[', not: '?') responsible for $expr_indent, if $expr_indent != 0
+my $in_block_decls;        # number of local declaration lines after block opening before normal statements, or -1 if no block opening
 my $in_expr;               # in expression after if/while/for/switch/return/enum/LHS of assignment
 my $in_paren_expr;         # in parenthesized if/while/for condition and switch expression, if $expr_indent != 0
 my $in_typedecl;           # nesting level of typedef/struct/union/enum
@@ -180,6 +193,7 @@ sub reset_file_state {
     $line = 0;
     $line_before = 0;
     $line_before2 = 0;
+    $code_contents_before = "";
     @nested_block_indents = ();
     @nested_hanging_offsets = ();
     @nested_in_typedecl = ();
@@ -187,8 +201,9 @@ sub reset_file_state {
     @nested_indents = ();
     @nested_conds_indents = ();
     $expr_indent = 0;
-    $in_paren_expr = 0;
+    $in_block_decls = -1;
     $in_expr = 0;
+    $in_paren_expr = 0;
     $hanging_offset = 0;
     @in_do_hanging_offsets = ();
     @in_if_hanging_offsets = ();
@@ -210,7 +225,7 @@ sub report_flexibly {
     my $line = shift;
     my $msg = shift;
     my $contents = shift;
-    my $report_SPC = $msg =~ /SPC/;
+    my $report_SPC = $msg =~ /space/;
     return if $report_SPC && $sloppy_SPC;
 
     print "$ARGV:$line:$msg:$contents" unless $self_test;
@@ -235,9 +250,9 @@ sub parens_balance { # count balance of opening parentheses - closing parenthese
 
 sub blind_nonspace { # blind non-space text of comment as @, preserving length and spaces
     # the @ character is used because it cannot occur in normal program code so there is no confusion
-    # comment text is not blinded to whitespace in order to be able to check double SPC also in comments
+    # comment text is not blinded to whitespace in order to be able to check extra SPC also in comments
     my $comment_text = shift;
-    $comment_text =~ s/\.\s\s/.. /g; # in double SPC checks allow one extra space after period '.' in comments
+    $comment_text =~ s/([\.\?\!])\s\s/$1. /g; # in extra SPC checks allow one extra SPC after period '.', '?', or '!' in comments
     return $comment_text =~ tr/ /@/cr;
 }
 
@@ -305,7 +320,7 @@ sub check_indent { # used for lines outside multi-line string literals
             $contents_before) if !$sloppy_cmt && $count_before != $count;
     }
     # ... but allow normal indentation for the current line, else above check will be done for the line before
-    if (($in_comment == 0 || $in_comment < 0) # (no commment,) intra-line comment or end of multi-line comment
+    if (($in_comment == 0 || $in_comment < 0) # (no comment,) intra-line comment or end of multi-line comment
         && m/^(\s*)@[\s@]*$/) { # line begins with '@', no code follows (except '\')
         if ($count == $ref_indent) { # indentation is like for (normal) code in this line
             s/^(\s*)@/$1*/; # blind first '@' as '*' to prevent above delayed check for the line before
@@ -366,6 +381,7 @@ sub update_nested_indents { # may reset $in_paren_expr and in this case also res
         my $in_stmt = $in_expr || @nested_symbols != 0; # not: || $in_typedecl != 0
         if ($c =~ m/[{([?]/) { # $c is '{', '(', '[', or '?'
             if ($c eq "{") { # '{' in any context
+                $in_block_decls = 0 if !$in_expr && $in_typedecl == 0;
                 # cancel newly hanging_offset if opening brace '{' is after non-whitespace non-comment:
                 $hanging_offset -= INDENT_LEVEL if $hanging_offset > 0 && $head =~ m/[^\s\@]/;
                 push @nested_block_indents, $block_indent;
@@ -447,6 +463,7 @@ reset_file_state();
 
 while (<>) { # loop over all lines of all input files
     $self_test = $ARGV =~ m/check-format-test/;
+    $_ = "" if $self_test && m/ blank line within local decls /;
     $line++;
     s/\r$//; # strip any trailing CR '\r' (which are typical on Windows systems)
     $contents = $_;
@@ -500,26 +517,26 @@ while (<>) { # loop over all lines of all input files
 
     # do/prepare checks within multi-line comments
     my $self_test_exception = $self_test ? "@" : "";
-    if ($in_comment > 0) { # this still includes the last line of multi-line commment
+    if ($in_comment > 0) { # this still includes the last line of multi-line comment
         my ($head, $any_symbol, $cmt_text) = m/^(\s*)(.?)(.*)$/;
         if ($any_symbol eq "*") {
-            report("no SPC after leading '*' in multi-line comment") if $cmt_text =~ m|^[^/\s$self_test_exception]|;
+            report("missing space or '*' after leading '*' in multi-line comment") if $cmt_text =~ m|^[^*\s/$self_test_exception]|;
         } else {
-            report("no leading '*' in multi-line comment");
+            report("missing leading '*' in multi-line comment");
         }
         $in_comment++;
     }
 
     # detect end of comment, must be within multi-line comment, check if it is preceded by non-whitespace text
     if ((my ($head, $tail) = m|^(.*?)\*/(.*)$|) && $1 ne '/') { # ending comment: '*/'
-        report("neither SPC nor '*' before '*/'") if $head =~ m/[^*\s]$/;
-        report("no SPC after '*/'") if $tail =~ m/^[^\s,;)}\]]/; # no space or ,;)}] after '*/'
+        report("neither space nor '*' before '*/'") if $head =~ m/[^*\s]$/;
+        report("missing space after '*/'") if $tail =~ m/^[^\s,;)}\]]/; # no space or ,;)}] after '*/'
         if (!($head =~ m|/\*|)) { # not begin of comment '/*', which is is handled below
             if ($in_comment == 0) {
                 report("unexpected '*/' outside comment");
                 $_ = "$head@@".$tail; # blind the "*/"
             } else {
-                report("text before '*/' in multi-line comment") if ($head =~ m/\S/); # non-SPC before '*/'
+                report("text before '*/' in multi-line comment") if ($head =~ m/[^*\s]/); # non-SPC before '*/'
                 $in_comment = -1; # indicate that multi-line comment ends on current line
                 if ($count > 0) {
                     # make indentation of end of multi-line comment appear like of leading intra-line comment
@@ -536,9 +553,9 @@ while (<>) { # loop over all lines of all input files
     # detect begin of comment, check if it is followed by non-space text
   MATCH_COMMENT:
     if (my ($head, $opt_minus, $tail) = m|^(.*?)/\*(-?)(.*)$|) { # begin of comment: '/*'
-        report("no SPC before '/*'")
+        report("missing space before '/*'")
             if $head =~ m/[^\s(\*]$/; # not space, '(', or or '*' (needed to allow '*/') before comment delimiter
-        report("neither SPC nor '*' after '/*' or '/*-'") if $tail =~ m/^[^\s*$self_test_exception]/;
+        report("missing space, '*' or '!' after '/*' or '/*-'") if $tail =~ m/^[^*\s!$self_test_exception]/;
         my $cmt_text = $opt_minus.$tail; # preliminary
         if ($in_comment > 0) {
             report("unexpected '/*' inside multi-line comment");
@@ -551,8 +568,8 @@ while (<>) { # loop over all lines of all input files
         } else { # begin of multi-line comment
             my $self_test_exception = $self_test ? "(@\d?)?" : "";
             report("text after '/*' in multi-line comment")
-                unless $tail =~ m/^$self_test_exception.?\s*$/;
-            # tail not essentially empty, first char already checked
+                unless $tail =~ m/^$self_test_exception.?[*\s]*$/;
+            # tail not essentially blank, first char already checked
             # adapt to actual indentation of first line
             $comment_indent = length($head) + 1;
             $_ = "$head@@".blind_nonspace($cmt_text);
@@ -560,6 +577,7 @@ while (<>) { # loop over all lines of all input files
             $leading_comment = $head =~ m/^\s*$/; # there is code before beginning delimiter
             $formatted_comment = $opt_minus eq "-";
         }
+    } elsif (($head, $tail) = m|^\{-(.*)$|) { # begin of Perl pragma: '{-'
     }
 
     if ($in_comment > 1) { # still inside multi-line comment (not at its begin or end)
@@ -594,23 +612,23 @@ while (<>) { # loop over all lines of all input files
 
     # at this point all non-space portions of any types of comments have been blinded as @
 
-    goto LINE_FINISHED if m/^\s*$/; # essentially empty line: just whitespace (and maybe a trailing '\')
+    goto LINE_FINISHED if m/^\s*$/; # essentially blank line: just whitespace (and maybe a trailing '\')
 
     # intra-line whitespace nits @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
 
     my $in_multiline_comment = ($in_comment > 1 || $in_comment < 0); # $in_multiline_comment refers to line before
     if (!$sloppy_SPC && !($in_multiline_comment && $formatted_comment)) {
-        sub dbl_SPC {
+        sub extra_SPC {
             my $intra_line = shift;
-            return "double SPC".($intra_line =~ m/@\s\s/ ?
-                                 $in_comment != 0 ? " in multi-line comment"
-                                                  : " in intra-line comment" : "");
+            return "extra space".($intra_line =~ m/@\s\s/ ?
+                                  $in_comment != 0 ? " in multi-line comment"
+                                                   : " in intra-line comment" : "");
         }
-        sub split_line_head {
+        sub split_line_head { # split line contents into header containing leading spaces and the first non-space char, and the rest of the line
             my $comment_symbol =
                 $in_comment != 0 ? "@" : ""; # '@' will match the blinded leading '*' in multi-line comment
                                              # $in_comment may pertain to the following line due to delayed check
-            # do not check for double SPC in leading spaces including any '#' (or '*' within multi-line comment)
+            # do not check for extra SPC in leading spaces including any '#' (or '*' within multi-line comment)
             shift =~ m/^(\s*([#$comment_symbol]\s*)?)(.*?)\s*$/;
             return ($1, $3);
         }
@@ -618,26 +636,29 @@ while (<>) { # loop over all lines of all input files
         my ($head1, $intra_line1) = split_line_head($contents_before_ ) if $line_before > 0;
         my ($head2, $intra_line2) = split_line_head($contents_before_2) if $line_before2 > 0;
         if ($line_before > 0) { # check with one line delay, such that at least $contents_before is available
-            sub column_alignments_only {
-                my $head = shift;
-                my $intra = shift;
-                my $contents = shift;
-                # check if all double SPC in $intra is used only for multi-line column alignment with $contents
+            sub column_alignments_only { # return 1 if the given line has multiple consecutive spaces only at columns that match the reference line
+                # all parameter strings are assumed to contain contents after blinding comments etc.
+                my $head = shift;     # leading spaces and the first non-space char
+                my $intra = shift;    # the rest of the line contents
+                my $contents = shift; # reference line
+                # check if all extra SPC in $intra is used only for multi-line column alignment with $contents
                 my $offset = length($head);
                 for (my $col = 0; $col < length($intra) - 2; $col++) {
-                   return 0 if substr($intra   , $col, 3) =~ m/\s\s\S/ # double space (after leading space)
-                          && !(substr($contents, $col + $offset + 1, 2) =~ m/\s\S/)
+                    my $substr = substr($intra, $col);
+                    next unless $substr =~ m/^\s\s\S/; # extra SPC (but not in leading spaces of the line)
+                    next if !$eol_cmt && $substr =~ m/^[@\s]+$/; # end-of-line comment
+                    return 0 unless substr($contents, $col + $offset + 1, 2) =~ m/\s\S/; # reference line contents do not match
                 }
                 return 1;
             }
-            report_flexibly($line_before, dbl_SPC($intra_line1), $contents_before) if $intra_line1 =~ m/\s\s\S/ &&
+            report_flexibly($line_before, extra_SPC($intra_line1), $contents_before) if $intra_line1 =~ m/\s\s\S/ &&
                !(    column_alignments_only($head1, $intra_line1, $_                )    # compare with $line
                  || ($line_before2 > 0 &&
                      column_alignments_only($head1, $intra_line1, $contents_before_2))); # compare w/ $line_before2
-            report(dbl_SPC($intra_line)) if $intra_line  =~ m/\s\s\S/ && eof
+            report(extra_SPC($intra_line)) if $intra_line  =~ m/\s\s\S/ && eof
                 && ! column_alignments_only($head , $intra_line , $contents_before_ )  ; # compare w/ $line_before
         } elsif (eof) { # special case: just one line exists
-            report(dbl_SPC($intra_line)) if $intra_line  =~ m/\s\s\S/;
+            report(extra_SPC($intra_line)) if $intra_line  =~ m/\s\s\S/;
         }
         # ignore paths in #include
         $intra_line =~ s/^(include\s*)(".*?"|<.*?>)/$1/e if $head =~ m/#/;
@@ -652,46 +673,48 @@ while (<>) { # loop over all lines of all input files
         # remove blinded comments etc. directly before ,;)}]
         while ($intra_line =~ s/\s?@+([,;\)\}\]])/$1/e) {} # /g does not work here
         # treat remaining blinded comments and string literal contents as (single) space during matching below
-        $intra_line =~ s/@+/ /g;                     # note that double SPC has already been handled above
+        $intra_line =~ s/@+/ /g;                     # note that extra SPC has already been handled above
         $intra_line =~ s/\s+$//;                     # strip any (resulting) space at EOL
-        $intra_line =~ s/(for\s*\();;(\))/"$1$2"/eg; # strip ';;' in for (;;)
+        $intra_line =~ s/(for\s*\([^;]*);;(\))/"$1$2"/eg; # strip trailing ';;' in for (;;)
+        $intra_line =~ s/(for\s*\([^;]+;[^;]+);(\))/"$1$2"/eg; # strip trailing ';' in for (;;)
+        $intra_line =~ s/(for\s*\();(;)/"$1$2"/eg;   # replace leading ';;' in for (;;) by ';'
         $intra_line =~ s/(=\s*)\{ /"$1@ "/eg;        # do not report {SPC in initializers such as ' = { 0, };'
         $intra_line =~ s/, \};/, @;/g;               # do not report SPC} in initializers such as ' = { 0, };'
-        report("SPC before '$1'") if $intra_line =~ m/[\w)\]]\s+(\+\+|--)/;  # postfix ++/-- with preceding space
-        report("SPC after '$1'")  if $intra_line =~ m/(\+\+|--)\s+[a-zA-Z_(]/; # prefix ++/-- with following space
+        report("space before '$1'") if $intra_line =~ m/[\w)\]]\s+(\+\+|--)/;  # postfix ++/-- with preceding space
+        report("space after '$1'")  if $intra_line =~ m/(\+\+|--)\s+[a-zA-Z_(]/; # prefix ++/-- with following space
         $intra_line =~ s/\.\.\./@/g;                 # blind '...'
-        report("SPC before '$1'") if $intra_line =~ m/\s(\.|->)/;            # '.' or '->' with preceding space
-        report("SPC after '$1'")  if $intra_line =~ m/(\.|->)\s/;            # '.' or '->' with following space
+        report("space before '$1'") if $intra_line =~ m/\s(\.|->)/;            # '.' or '->' with preceding space
+        report("space after '$1'")  if $intra_line =~ m/(\.|->)\s/;            # '.' or '->' with following space
         $intra_line =~ s/\-\>|\+\+|\-\-/@/g;         # blind '->,', '++', and '--'
-        report("SPC before '$2'")     if $intra_line =~ m/[^:]\s+(;)/;       # space before ';' but not after ':'
-        report("SPC before '$1'")     if $intra_line =~ m/\s([,)\]])/;       # space before ,)]
-        report("SPC after '$1'")      if $intra_line =~ m/([(\[~!])\s/;      # space after ([~!
-        report("SPC after '$1'")      if $intra_line =~ m/(defined)\s/;      # space after 'defined'
-        report("no SPC before '=' or '<op>='") if $intra_line =~ m/\S(=)/;   # '=' etc. without preceding space
-        report("no SPC before '$1'")  if $intra_line =~ m/\S([|\/%<>^\?])/;  # |/%<>^? without preceding space
+        report("space before '$1'")     if $intra_line =~ m/[^:)]\s+(;)/;      # space before ';' but not after ':' or ')'
+        report("space before '$1'")     if $intra_line =~ m/\s([,)\]])/;       # space before ,)]
+        report("space after '$1'")      if $intra_line =~ m/([(\[~!])\s/;      # space after ([~!
+        report("space after '$1'")      if $intra_line =~ m/(defined)\s/;      # space after 'defined'
+        report("missing space before '=' or '<op>='") if $intra_line =~ m/\S(=)/;   # '=' etc. without preceding space
+        report("missing space before '$1'")  if $intra_line =~ m/\S([|\/%<>^\?])/;  # |/%<>^? without preceding space
         # TODO ternary ':' without preceding SPC, while allowing no SPC before ':' after 'case'
-        report("no SPC before '$1'")  if $intra_line =~ m/[^\s{()\[]([+\-])/;# +/- without preceding space or {()[
-                                                                             # or ')' (which is used f type casts)
-        report("no SPC before '$1'")  if $intra_line =~ m/[^\s{()\[*]([*])/; # '*' without preceding space or {()[*
-        report("no SPC before '$1'")  if $intra_line =~ m/[^\s{()\[]([&])/;  # '&' without preceding space or {()[
-        report("no SPC after ternary '$1'") if $intra_line =~ m/(:)[^\s\d]/; # ':' without following space or digit
-        report("no SPC after '$1'")   if $intra_line =~ m/([,;=|\/%<>^\?])\S/; # ,;=|/%<>^? without following space
-        report("no SPC after binary '$1'") if $intra_line=~m/([*])[^\sa-zA-Z_(),*]/;# '*' w/o space or \w(),* after
+        report("missing space before binary '$2'")  if $intra_line =~ m/([^\s{()\[e])([+\-])/; # '+'/'-' without preceding space or {()[e
+        # ')' may be used for type casts or before "->", 'e' may be used for numerical literals such as "1e-6"
+        report("missing space before binary '$1'")  if $intra_line =~ m/[^\s{()\[*!]([*])/; # '*' without preceding space or {()[*!
+        report("missing space before binary '$1'")  if $intra_line =~ m/[^\s{()\[]([&])/;  # '&' without preceding space or {()[
+        report("missing space after ternary '$1'") if $intra_line =~ m/(:)[^\s\d]/; # ':' without following space or digit
+        report("missing space after '$1'")   if $intra_line =~ m/([,;=|\/%<>^\?])\S/; # ,;=|/%<>^? without following space
+        report("missing space after binary '$1'") if $intra_line=~m/[^{(\[]([*])[^\sa-zA-Z_(),*]/;# '*' w/o space or \w(),* after
         # TODO unary '*' must not be followed by SPC
-        report("no SPC after binary '$1'") if $intra_line=~m/([&])[^\sa-zA-Z_(]/;  # '&' w/o following space or \w(
+        report("missing space after binary '$1'") if $intra_line=~m/([&])[^\sa-zA-Z_(]/;  # '&' w/o following space or \w(
         # TODO unary '&' must not be followed by SPC
-        report("no SPC after binary '$1'") if $intra_line=~m/([+\-])[^\s\d(]/;  # +/- w/o following space or \d(
+        report("missing space after binary '$1'") if $intra_line=~m/[^{(\[]([+\-])[^\s\d(]/;  # +/- w/o following space or \d(
         # TODO unary '+' and '-' must not be followed by SPC
-        report("no SPC after '$2'")   if $intra_line =~ m/(^|\W)(if|while|for|switch|case)[^\w\s]/; # kw w/o SPC
-        report("no SPC after '$2'")   if $intra_line =~ m/(^|\W)(return)[^\w\s;]/;  # return w/o SPC or ';'
-        report("SPC after function/macro name")
+        report("missing space after '$2'")   if $intra_line =~ m/(^|\W)(if|while|for|switch|case)[^\w\s]/; # kw w/o SPC
+        report("missing space after '$2'")   if $intra_line =~ m/(^|\W)(return)[^\w\s;]/;  # return w/o SPC or ';'
+        report("space after function/macro name")
                                       if $intra_line =~ m/(\w+)\s+\(/        # fn/macro name with space before '('
-       && !($1 =~ m/^(if|while|for|switch|return|typedef|void|char|unsigned|int|long|float|double)$/) # not keyword
+       && !($1 =~ m/^(sizeof|if|else|while|do|for|switch|case|default|break|continue|goto|return|void|char|signed|unsigned|int|short|long|float|double|typedef|enum|struct|union|auto|extern|static|const|volatile|register)$/) # not keyword
                                     && !(m/^\s*#\s*define\s/); # we skip macro definitions here because macros
                                     # without parameters but with body beginning with '(', e.g., '#define X (1)',
                                     # would lead to false positives - TODO also check for macros with parameters
-        report("no SPC before '{'")   if $intra_line =~ m/[^\s{(\[]\{/;      # '{' without preceding space or {([
-        report("no SPC after '}'")    if $intra_line =~ m/\}[^\s,;\])}]/;    # '}' without following space or ,;])}
+        report("missing space before '{'")   if $intra_line =~ m/[^\s{(\[]\{/;      # '{' without preceding space or {([
+        report("missing space after '}'")    if $intra_line =~ m/\}[^\s,;\])}]/;    # '}' without following space or ,;])}
     }
 
     # preprocessor directives @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@ -723,7 +746,8 @@ while (<>) { # loop over all lines of all input files
     # update indents according to leading closing brace(s) '}' or label or switch case
     my $in_stmt = $in_expr || @nested_symbols != 0 || $in_typedecl != 0;
     if ($in_stmt) { # expr/stmt/type decl/var def/fn hdr, i.e., not at block level
-        if (m/^([\s@]*\})/) { # leading '}', any preceding blinded comment must not be matched
+        if (m/^([\s@]*\})/) { # leading '}' within stmt, any preceding blinded comment must not be matched
+            $in_block_decls = -1;
             my $head = $1;
             update_nested_indents($head);
             $nested_indents_position = length($head);
@@ -770,7 +794,8 @@ while (<>) { # loop over all lines of all input files
             }
             if ($before ne "") { # non-whitespace non-'{' before '}'
                 report("code before '}'");
-            } else { # leading '}', any preceding blinded comment must not be matched
+            } else { # leading '}' outside stmt, any preceding blinded comment must not be matched
+                $in_block_decls = -1;
                 $local_offset = $block_indent + $hanging_offset - INDENT_LEVEL;
                 update_nested_indents($head);
                 $nested_indents_position = length($head);
@@ -793,7 +818,7 @@ while (<>) { # loop over all lines of all input files
             $local_offset = -INDENT_LEVEL;
         } else {
             if (m/^([\s@]*)(\w+):/) { # (leading) label, cannot be "default"
-                $local_offset = -INDENT_LEVEL + 1 ;
+                $local_offset = -INDENT_LEVEL;
                 $has_label = 1;
             }
         }
@@ -817,6 +842,27 @@ while (<>) { # loop over all lines of all input files
 
     check_indent() if $count >= 0; # not for #define and not if multi-line string literal is continued
 
+    # check for blank lines within/after local decls @@@@@@@@@@@@@@@@@@@@@@@@@@@
+
+    if ($in_block_decls >= 0 &&
+        $in_comment == 0 && !m/^\s*\*?@/ && # not in multi-line comment nor an intra-line comment
+        !$in_expr && $expr_indent == 0 && $in_typedecl == 0) {
+        my $blank_line_before = $line > 1
+            && $code_contents_before =~ m/^\s*(\\\s*)?$/; # essentially blank line: just whitespace (and maybe a trailing '\')
+        if (m/^[\s(]*(char|signed|unsigned|int|short|long|float|double|enum|struct|union|auto|extern|static|const|volatile|register)(\W|$)/ # clear start of local decl
+            || (m/^(\s*(\w+|\[\]|[\*()]))+?\s+[\*\(]*\w+(\s*(\)|\[[^\]]*\]))*\s*[;,=]/ # weak check for decl involving user-defined type
+                && !m/^\s*(\}|sizeof|if|else|while|do|for|switch|case|default|break|continue|goto|return)(\W|$)/)) {
+            $in_block_decls++;
+            report_flexibly($line - 1, "blank line within local decls, before", $contents) if $blank_line_before;
+        } else {
+            report_flexibly($line, "missing blank line after local decls", "\n$contents_before$contents")
+                if $in_block_decls > 0 && !$blank_line_before;
+            $in_block_decls = -1 unless
+                m/^\s*(\\\s*)?$/ # essentially blank line: just whitespace (and maybe a trailing '\')
+            || $in_comment != 0 || m/^\s*\*?@/; # in multi-line comment or an intra-line comment
+        }
+    }
+
     $in_comment = 0 if $in_comment < 0; # multi-line comment has ended
 
     # do some further checks @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@ -836,16 +882,18 @@ while (<>) { # loop over all lines of all input files
              $line_opening_brace == $line_before)
             && $contents_before =~ m/;/) { # there is at least one terminator ';', so there is some stmt
             # TODO do not report cases where a further else branch
-            # follows with a block containg more than one line/statement
+            # follows with a block containing more than one line/statement
             report_flexibly($line_before, "'$keyword_opening_brace' { 1 stmt }", $contents_before);
         }
     }
 
-    report("one-letter name '$2'") if (m/(^|.*\W)([lIO])(\W.*|$)/); # single-letter name 'l', 'I', or 'O'
-
-    # TODO report empty line within local variable definitions
+    report("single-letter name '$2'") if (m/(^|.*\W)([IO])(\W.*|$)/); # single-letter name 'I' or 'O' # maybe re-add 'l'?
+    # constant on LHS of comparison or assignment, e.g., NULL != x or 'a' < c, but not a + 1 == b
+    report("constant on LHS of '$3'")
+        if (m/(['"]|([\+\-\*\/\/%\&\|\^<>]\s*)?\W[0-9]+L?|\WNULL)\s*([\!<>=]=|[<=>])([<>]?)/ &&
+            $2 eq "" && (($3 ne "<" && $3 ne "='" && $3 ne ">") || $4 eq ""));
 
-    # TODO report missing empty line after local variable definitions
+    # TODO report #if 0 and #if 1
 
     # TODO report needless use of parentheses, while
     #      macro parameters should always be in parens (except when passed on), e.g., '#define ID(x) (x)'
@@ -913,10 +961,12 @@ while (<>) { # loop over all lines of all input files
     }
 
     # set $in_typedecl and potentially $hanging_offset for type declaration
-    if (!$in_expr && @nested_indents == 0 && # not in expression
-        m/(^|^.*\W)(typedef|struct|union|enum)(\W.*|$)$/ &&
-        parens_balance($1) == 0) { # not in newly started expression
-        # not needed: $keyword_opening_brace = $2 if $3 =~ m/\{/;
+    if (!$in_expr && @nested_indents == 0 # not in expression
+        && m/(^|^.*\W)(typedef|enum|struct|union)(\W.*|$)$/
+        && parens_balance($1) == 0 # not in newly started expression or function arg list
+        && ($2 eq "typedef" || !($3 =~ m/\s*\w++\s*(.)/ && $1 ne "{")) # 'struct'/'union'/'enum' <name> not followed by '{'
+        # not needed: && $keyword_opening_brace = $2 if $3 =~ m/\{/;
+        ) {
         $in_typedecl++;
         $hanging_offset += INDENT_LEVEL if m/\*.*\(/; # '*' followed by '(' - seems consistent with Emacs C mode
     }
@@ -996,12 +1046,12 @@ while (<>) { # loop over all lines of all input files
                     !($keyword_opening_brace eq "else" && $line_opening_brace < $line_before2);
             }
             report("code after '{'") if $tail=~ m/[^\s\@]/ && # trailing non-whitespace non-comment (non-'\')
-                                      !($tail=~ m/\}/);  # no '}' after last '{'
+                                      !($tail=~ m/\}/);  # missing '}' after last '{'
         }
     }
 
     # check for opening brace after if/while/for/switch/do not on same line
-    # note that "no '{' on same line after '} else'" is handled further below
+    # note that "missing '{' on same line after '} else'" is handled further below
     if (/^[\s@]*{/ && # leading '{'
         $line_before > 0 && !($contents_before_ =~ m/^\s*#/) && # not preprocessor directive '#if
         (my ($head, $mid, $tail) = ($contents_before_ =~ m/(^|^.*\W)(if|while|for|switch|do)(\W.*$|$)/))) {
@@ -1011,10 +1061,10 @@ while (<>) { # loop over all lines of all input files
     # check for closing brace on line before 'else' not followed by leading '{'
     elsif (my ($head, $tail) = m/(^|^.*\W)else(\W.*$|$)/) {
         if (parens_balance($tail) == 0 &&  # avoid false positive due to unfinished expr on current line
-            !($tail =~ m/{/) && # after 'else' no '{' on same line
+            !($tail =~ m/{/) && # after 'else' missing '{' on same line
             !($head =~ m/}[\s@]*$/) && # not: '}' then any whitespace or comments before 'else'
             $line_before > 0 && $contents_before_ =~ /}[\s@]*$/) { # trailing '}' on line before
-            report("no '{' after '} else'");
+            report("missing '{' on same line after '} else'");
         }
     }
 
@@ -1041,10 +1091,10 @@ while (<>) { # loop over all lines of all input files
             if ($line_before > 0 && $contents_before_ =~ /}[\s@]*$/) {
                 report("'else' not on same line as preceding '}'");
             } elsif (parens_balance($tail) == 0) { # avoid false positive due to unfinished expr on current line
-                report("no '}' on same line before 'else ... {'") if $brace_after;
+                report("missing '}' on same line before 'else ... {'") if $brace_after;
             }
         } elsif (parens_balance($tail) == 0) { # avoid false positive due to unfinished expr on current line
-            report("no '{' on same line after '} else'") if $brace_before && !$brace_after;
+            report("missing '{' on same line after '} else'") if $brace_before && !$brace_after;
         }
     }
 
@@ -1064,6 +1114,10 @@ while (<>) { # loop over all lines of all input files
     # post-processing at end of line @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
 
   LINE_FINISHED:
+    $code_contents_before = $contents if
+        !m/^\s*#(\s*)(\w+)/ && # not single-line directive
+        $in_comment == 0 && !m/^\s*\*?@/; # not in multi-line comment nor an intra-line comment
+
     # on end of multi-line preprocessor directive, adapt indent
     if ($in_directive > 0 &&
         # need to use original line contents because trailing \ may have been stripped
@@ -1074,12 +1128,12 @@ while (<>) { # loop over all lines of all input files
         $hanging_offset = 0; # compensate for this in case macro ends, e.g., as 'while (0)'
     }
 
-    if (m/^\s*$/) { # essentially empty line: just whitespace (and maybe a '\')
-            report("empty line at beginnig of file") if $line == 1 && !$sloppy_SPC;
+    if (m/^\s*$/) { # at begin of file essentially blank line: just whitespace (and maybe a '\')
+            report("leading ".($1 eq "" ? "blank" :"whitespace")." line") if $line == 1 && !$sloppy_SPC;
     } else {
         if ($line_before > 0) {
             my $linediff = $line - $line_before - 1;
-            report("$linediff empty lines before") if $linediff > 1 && !$sloppy_SPC;
+            report("$linediff blank lines before") if $linediff > 1 && !$sloppy_SPC;
         }
         $line_before2      = $line_before;
         $contents_before2  = $contents_before;
@@ -1101,8 +1155,8 @@ while (<>) { # loop over all lines of all input files
     # post-processing at end of file @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
 
     if (eof) {
-        # check for essentially empty line (which may include a '\') just before EOF
-        report(($1 eq "\n" ? "empty line" : $2 ne "" ? "'\\'" : "whitespace")." at EOF")
+        # check for essentially blank line (which may include a '\') just before EOF
+        report(($1 eq "\n" ? "blank line" : $2 ne "" ? "'\\'" : "whitespace")." at EOF")
             if $contents =~ m/^(\s*(\\?)\s*)$/ && !$sloppy_SPC;
 
         # report unclosed expression-level nesting