C/C++ style checking script from erik de castro-lopo, for use in client-side pre...
[ardour.git] / tools / cstyle.py
1 #!/usr/bin/python -tt
2 #
3 # Copyright (C) 2005-2012 Erik de Castro Lopo <erikd@mega-nerd.com>
4 #
5 # Released under the 2 clause BSD license.
6
7 """
8 This program checks C code for compliance to coding standards used in
9 libsndfile and other projects I run.
10 """
11
12 import re
13 import sys
14
15
16 class 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
101 class 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 ("^ "),          "leading space as indentation instead of tab - use tabs to indent, spaces to align" )                   
117                         , ( re.compile ("{[^\s]"),      "missing space after open brace" )                                                                      
118                         , ( re.compile ("[^\s]}"),      "missing space before close brace" )                                                                    
119                         , ( re.compile ("[ \t]+$"),     "contains trailing whitespace" )                                                                        
120                                                                                                                                                                 
121                         , ( re.compile (",[^\s\n]"),            "missing space after comma" )                                                                   
122                         , ( re.compile (";[a-zA-Z0-9]"),        "missing space after semi-colon" )                                                              
123                         , ( re.compile ("=[^\s\"'=]"),          "missing space after assignment" )                                                              
124                                                                                                                                                                 
125                         # Open and close parenthesis.                                                                                                           
126                         , ( re.compile ("[^\s\(\[\*&']\("),             "missing space before open parenthesis" )                                               
127                         , ( re.compile ("\)(-[^>]|[^,'\s\n\)\]-])"),    "missing space after close parenthesis" )                                               
128                         , ( re.compile ("\( [^;]"),                     "space after open parenthesis" )                                                        
129                         , ( re.compile ("[^;] \)"),                     "space before close parenthesis" )                                                      
130                                                                                                                                                                 
131                         # Open and close square brace.                                                                                                          
132                         , ( re.compile ("\[ "),                                 "space after open square brace" )                                               
133                         , ( re.compile (" \]"),                                 "space before close square brace" )                                             
134                                                                                                                                                                 
135                         # Space around operators.                                                                                                               
136                         , ( re.compile ("[^\s][\*/%+-][=][^\s]"),               "missing space around opassign" )                                               
137                         , ( re.compile ("[^\s][<>!=^/][=]{1,2}[^\s]"),  "missing space around comparison" )                                                     
138                                                                                                                                                                 
139                         # Parens around single argument to return.                                                                                              
140                         , ( re.compile ("\s+return\s+\([a-zA-Z0-9_]+\)\s+;"),   "parens around return value" )                                                  
141                         ]                                                                                                                                       
142
143
144         def get_error_count (self):
145                 """
146                 Return the current error count for this CStyleChecker object.
147                 """
148                 return self.error_count
149
150         def check_files (self, files):
151                 """
152                 Run the style checker on all the specified files.
153                 """
154                 for filename in files:
155                         self.check_file (filename)
156
157         def check_file (self, filename):
158                 """
159                 Run the style checker on the specified file.
160                 """
161                 self.filename = filename
162                 try:
163                         cfile = open (filename, "r")
164                 except IOError as e:
165                         return
166
167                 self.line_num = 1
168
169                 preprocess = Preprocessor ()
170                 while 1:
171                         line = cfile.readline ()
172                         if not line:
173                                 break
174
175                         line = self.trailing_newline_re.sub ('', line)
176                         self.orig_line = line
177
178                         self.line_checks (preprocess (line))
179
180                         self.line_num += 1
181
182                 cfile.close ()
183                 self.filename = None
184
185                 # Check for errors finding comments.
186                 if preprocess.comment_nesting () != 0:
187                         print ("Weird, comments nested incorrectly.")
188                         sys.exit (1)
189
190                 return
191
192         def line_checks (self, line):
193                 """
194                 Run the style checker on provided line of text, but within the context
195                 of how the line fits within the file.
196                 """
197
198                 indent = len (self.indent_re.search (line).group ())
199                 if re.search ("^\s+}", line):
200                         if not self.last_line_indent_curly and indent != self.last_line_indent:
201                                 None    # self.error ("bad indent on close curly brace")
202                         self.last_line_indent_curly = True
203                 else:
204                         self.last_line_indent_curly = False
205
206                 # Now all the regex checks.
207                 for (check_re, msg) in self.re_checks:
208                         if check_re.search (line):
209                                 self.error (msg)
210
211                 if re.search ("[a-zA-Z0-9][<>!=^/&\|]{1,2}[a-zA-Z0-9]", line):
212                         if not re.search (".*#include.*[a-zA-Z0-9]/[a-zA-Z]", line):
213                                 self.error ("missing space around operator")
214
215                 self.last_line_indent = indent
216                 return
217
218         def error (self, msg):
219                 """
220                 Print an error message and increment the error count.
221                 """
222                 print ("%s (%d) : %s" % (self.filename, self.line_num, msg))
223                 if self.debug:
224                         print ("'" + self.orig_line + "'")
225                 self.error_count += 1
226
227 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
228
229 if len (sys.argv) < 1:
230         print ("Usage : yada yada")
231         sys.exit (1)
232
233 # Create a new CStyleChecker object
234 if sys.argv [1] == '-d' or sys.argv [1] == '--debug':
235         cstyle = CStyleChecker (True)
236         cstyle.check_files (sys.argv [2:])
237 else:
238         cstyle = CStyleChecker (False)
239         cstyle.check_files (sys.argv [1:])
240
241
242 if cstyle.get_error_count ():
243         sys.exit (1)
244
245 sys.exit (0)