1#!/usr/bin/python -tt 2# 3# Copyright (C) 2005-2017 Erik de Castro Lopo <erikd@mega-nerd.com> 4# 5# Released under the 2 clause BSD license. 6 7""" 8This program checks C code for compliance to coding standards used in 9libsndfile and other projects I run. 10""" 11 12import re 13import sys 14 15 16class Preprocessor: 17 """ 18 Preprocess lines of C code to make it easier for the CStyleChecker class to 19 test for correctness. Preprocessing works on a single line at a time but 20 maintains state between consecutive lines so it can preprocessess multi-line 21 comments. 22 Preprocessing involves: 23 - Strip C++ style comments from a line. 24 - Strip C comments from a series of lines. When a C comment starts and 25 ends on the same line it will be replaced with 'comment'. 26 - Replace arbitrary C strings with the zero length string. 27 - Replace '#define f(x)' with '#define f (c)' (The C #define requires that 28 there be no space between defined macro name and the open paren of the 29 argument list). 30 Used by the CStyleChecker class. 31 """ 32 def __init__ (self): 33 self.comment_nest = 0 34 self.leading_space_re = re.compile ('^(\t+| )') 35 self.trailing_space_re = re.compile ('(\t+| )$') 36 self.define_hack_re = re.compile ("(#\s*define\s+[a-zA-Z0-9_]+)\(") 37 38 def comment_nesting (self): 39 """ 40 Return the currect comment nesting. At the start and end of the file, 41 this value should be zero. Inside C comments it should be 1 or 42 (possibly) more. 43 """ 44 return self.comment_nest 45 46 def __call__ (self, line): 47 """ 48 Strip the provided line of C and C++ comments. Stripping of multi-line 49 C comments works as expected. 50 """ 51 52 line = self.define_hack_re.sub (r'\1 (', line) 53 54 line = self.process_strings (line) 55 56 # Strip C++ style comments. 57 if self.comment_nest == 0: 58 line = re.sub ("( |\t*)//.*", '', line) 59 60 # Strip C style comments. 61 open_comment = line.find ('/*') 62 close_comment = line.find ('*/') 63 64 if self.comment_nest > 0 and close_comment < 0: 65 # Inside a comment block that does not close on this line. 66 return "" 67 68 if open_comment >= 0 and close_comment < 0: 69 # A comment begins on this line but doesn't close on this line. 70 self.comment_nest += 1 71 return self.trailing_space_re.sub ('', line [:open_comment]) 72 73 if open_comment < 0 and close_comment >= 0: 74 # Currently open comment ends on this line. 75 self.comment_nest -= 1 76 return self.trailing_space_re.sub ('', line [close_comment + 2:]) 77 78 if open_comment >= 0 and close_comment > 0 and self.comment_nest == 0: 79 # Comment begins and ends on this line. Replace it with 'comment' 80 # so we don't need to check whitespace before and after the comment 81 # we're removing. 82 newline = line [:open_comment] + "comment" + line [close_comment + 2:] 83 return self.__call__ (newline) 84 85 return line 86 87 def process_strings (self, line): 88 """ 89 Given a line of C code, return a string where all literal C strings have 90 been replaced with the empty string literal "". 91 """ 92 for k in range (0, len (line)): 93 if line [k] == '"': 94 start = k 95 for k in range (start + 1, len (line)): 96 if line [k] == '"' and line [k - 1] != '\\': 97 return line [:start + 1] + '"' + self.process_strings (line [k + 1:]) 98 return line 99 100 101class CStyleChecker: 102 """ 103 A class for checking the whitespace and layout of a C code. 104 """ 105 def __init__ (self, debug): 106 self.debug = debug 107 self.filename = None 108 self.error_count = 0 109 self.line_num = 1 110 self.orig_line = '' 111 self.trailing_newline_re = re.compile ('[\r\n]+$') 112 self.indent_re = re.compile ("^\s*") 113 self.last_line_indent = "" 114 self.last_line_indent_curly = False 115 self.re_checks = \ 116 [ ( re.compile (" "), "multiple space instead of tab" ) 117 , ( re.compile ("\t "), "space after tab" ) 118 , ( re.compile ("[^ ];"), "missing space before semi-colon" ) 119 , ( re.compile ("{[^\s}]"), "missing space after open brace" ) 120 , ( re.compile ("[^{\s]}"), "missing space before close brace" ) 121 , ( re.compile ("[ \t]+$"), "contains trailing whitespace" ) 122 123 , ( re.compile (",[^\s\n]"), "missing space after comma" ) 124 , ( re.compile (";[^\s]"), "missing space after semi-colon" ) 125 , ( re.compile ("=[^\s\"'=]"), "missing space after assignment" ) 126 127 # Open and close parenthesis. 128 , ( re.compile ("[^\s\(\[\*&']\("), "missing space before open parenthesis" ) 129 , ( re.compile ("\)(-[^>]|[^,'\s\n\)\]-])"), "missing space after close parenthesis" ) 130 , ( re.compile ("\s(do|for|if|when)\s.*{$"), "trailing open parenthesis at end of line" ) 131 , ( re.compile ("\( [^;]"), "space after open parenthesis" ) 132 , ( re.compile ("[^;] \)"), "space before close parenthesis" ) 133 134 # Open and close square brace. 135 , ( re.compile ("[^\s\(\]]\["), "missing space before open square brace" ) 136 , ( re.compile ("\][^,\)\]\[\s\.-]"), "missing space after close square brace" ) 137 , ( re.compile ("\[ "), "space after open square brace" ) 138 , ( re.compile (" \]"), "space before close square brace" ) 139 140 # Space around operators. 141 , ( re.compile ("[^\s][\*/%+-][=][^\s]"), "missing space around opassign" ) 142 , ( re.compile ("[^\s][<>!=^/][=]{1,2}[^\s]"), "missing space around comparison" ) 143 144 # Parens around single argument to return. 145 , ( re.compile ("\s+return\s+\([a-zA-Z0-9_]+\)\s+;"), "parens around return value" ) 146 147 # Parens around single case argument. 148 , ( re.compile ("\s+case\s+\([a-zA-Z0-9_]+\)\s+:"), "parens around single case argument" ) 149 150 # Open curly at end of line. 151 , ( re.compile ("\)\s*{\s*$"), "open curly brace at end of line" ) 152 153 # Pre and post increment/decrment. 154 , ( re.compile ("[^\(\[][+-]{2}[a-zA-Z0-9_]"), "space after pre increment/decrement" ) 155 , ( re.compile ("[a-zA-Z0-9_][+-]{2}[^\)\,]]"), "space before post increment/decrement" ) 156 ] 157 158 def get_error_count (self): 159 """ 160 Return the current error count for this CStyleChecker object. 161 """ 162 return self.error_count 163 164 def check_files (self, files): 165 """ 166 Run the style checker on all the specified files. 167 """ 168 for filename in files: 169 self.check_file (filename) 170 171 def check_file (self, filename): 172 """ 173 Run the style checker on the specified file. 174 """ 175 self.filename = filename 176 cfile = open (filename, "r") 177 178 self.line_num = 1 179 180 preprocess = Preprocessor () 181 while 1: 182 line = cfile.readline () 183 if not line: 184 break 185 186 line = self.trailing_newline_re.sub ('', line) 187 self.orig_line = line 188 189 self.line_checks (preprocess (line)) 190 191 self.line_num += 1 192 193 cfile.close () 194 self.filename = None 195 196 # Check for errors finding comments. 197 if preprocess.comment_nesting () != 0: 198 print ("Weird, comments nested incorrectly.") 199 sys.exit (1) 200 201 return 202 203 def line_checks (self, line): 204 """ 205 Run the style checker on provided line of text, but within the context 206 of how the line fits within the file. 207 """ 208 209 indent = len (self.indent_re.search (line).group ()) 210 if re.search ("^\s+}", line): 211 if not self.last_line_indent_curly and indent != self.last_line_indent: 212 None # self.error ("bad indent on close curly brace") 213 self.last_line_indent_curly = True 214 else: 215 self.last_line_indent_curly = False 216 217 # Now all the regex checks. 218 for (check_re, msg) in self.re_checks: 219 if check_re.search (line): 220 self.error (msg) 221 222 if re.search ("[a-zA-Z0-9][<>!=^/&\|]{1,2}[a-zA-Z0-9]", line): 223 if not re.search (".*#include.*[a-zA-Z0-9]/[a-zA-Z]", line): 224 self.error ("missing space around operator") 225 226 self.last_line_indent = indent 227 return 228 229 def error (self, msg): 230 """ 231 Print an error message and increment the error count. 232 """ 233 print ("%s (%d) : %s" % (self.filename, self.line_num, msg)) 234 if self.debug: 235 print ("'" + self.orig_line + "'") 236 self.error_count += 1 237 238#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 239 240if len (sys.argv) < 1: 241 print ("Usage : yada yada") 242 sys.exit (1) 243 244# Create a new CStyleChecker object 245if sys.argv [1] == '-d' or sys.argv [1] == '--debug': 246 cstyle = CStyleChecker (True) 247 cstyle.check_files (sys.argv [2:]) 248else: 249 cstyle = CStyleChecker (False) 250 cstyle.check_files (sys.argv [1:]) 251 252 253if cstyle.get_error_count (): 254 sys.exit (1) 255 256sys.exit (0) 257