don't interpret C++ templates with indirection/pointer/reference operators as operators
[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]+$"),     "empty line contains whitespace" )
120                           , ( re.compile ("[^\s][ \t]+$"),     "contains trailing whitespace" )
121
122                           , ( re.compile (",[^\s\n]"),            "missing space after comma" )
123                           , ( re.compile (";[a-zA-Z0-9]"),        "missing space after semi-colon" )
124                           , ( re.compile ("=[^\s\"'=]"),          "missing space after assignment" )
125                           
126                           # Open and close parenthesis.
127                           , ( re.compile ("[^_\s\(\[\*&']\("),             "missing space before open parenthesis" )
128                           , ( re.compile ("\)(-[^>]|[^;,'\s\n\)\]-])"),    "missing space after close parenthesis" )
129                           , ( re.compile ("\( [^;]"),                     "space after open parenthesis" )
130                           , ( re.compile ("[^;] \)"),                     "space before close parenthesis" )
131
132                           # Open and close square brace.
133                           , ( re.compile ("\[ "),                                 "space after open square brace" )
134                           , ( re.compile (" \]"),                                 "space before close square brace" )
135
136                           # Space around operators.
137                           , ( re.compile ("[^\s][\*/%+-][=][^\s]"),               "missing space around opassign" )
138                           , ( re.compile ("[^\s][<>!=^/][=]{1,2}[^\s]"),  "missing space around comparison" )
139
140                           # Parens around single argument to return.
141                           , ( re.compile ("\s+return\s+\([a-zA-Z0-9_]+\)\s+;"),   "parens around return value" )
142                         ]                                                                                                                                       
143
144
145         def get_error_count (self):
146                 """
147                 Return the current error count for this CStyleChecker object.
148                 """
149                 return self.error_count
150
151         def check_files (self, files):
152                 """
153                 Run the style checker on all the specified files.
154                 """
155                 for filename in files:
156                         self.check_file (filename)
157
158         def check_file (self, filename):
159                 """
160                 Run the style checker on the specified file.
161                 """
162                 self.filename = filename
163                 try:
164                         cfile = open (filename, "r")
165                 except IOError as e:
166                         return
167
168                 self.line_num = 1
169
170                 preprocess = Preprocessor ()
171                 while 1:
172                         line = cfile.readline ()
173                         if not line:
174                                 break
175
176                         line = self.trailing_newline_re.sub ('', line)
177                         self.orig_line = line
178
179                         self.line_checks (preprocess (line))
180
181                         self.line_num += 1
182
183                 cfile.close ()
184                 self.filename = None
185
186                 # Check for errors finding comments.
187                 if preprocess.comment_nesting () != 0:
188                         print ("Weird, comments nested incorrectly.")
189                         sys.exit (1)
190
191                 return
192
193         def line_checks (self, line):
194                 """
195                 Run the style checker on provided line of text, but within the context
196                 of how the line fits within the file.
197                 """
198
199                 indent = len (self.indent_re.search (line).group ())
200                 if re.search ("^\s+}", line):
201                         if not self.last_line_indent_curly and indent != self.last_line_indent:
202                                 None    # self.error ("bad indent on close curly brace")
203                         self.last_line_indent_curly = True
204                 else:
205                         self.last_line_indent_curly = False
206
207                 # Now all the regex checks.
208                 for (check_re, msg) in self.re_checks:
209                         if check_re.search (line):
210                                 self.error (msg)
211
212                 if re.search ("[a-zA-Z0-9][<>!=^/&\|]{1,2}[a-zA-Z0-9]", line):
213                         # ignore #include <foo.h> and C++ templates with indirection/pointer/reference operators
214                         if not re.search (".*#include.*[a-zA-Z0-9]/[a-zA-Z]", line) and not re.search ("[a-zA-Z0-9_]>[&\*]", line):
215                                 self.error ("missing space around operator")
216
217                 self.last_line_indent = indent
218                 return
219
220         def error (self, msg):
221                 """
222                 Print an error message and increment the error count.
223                 """
224                 print ("%s (%d) : %s" % (self.filename, self.line_num, msg))
225                 if self.debug:
226                         print ("'" + self.orig_line + "'")
227                 self.error_count += 1
228
229 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
230
231 if len (sys.argv) < 1:
232         print ("Usage : yada yada")
233         sys.exit (1)
234
235 # Create a new CStyleChecker object
236 if sys.argv [1] == '-d' or sys.argv [1] == '--debug':
237         cstyle = CStyleChecker (True)
238         cstyle.check_files (sys.argv [2:])
239 else:
240         cstyle = CStyleChecker (False)
241         cstyle.check_files (sys.argv [1:])
242
243
244 if cstyle.get_error_count ():
245         sys.exit (1)
246
247 sys.exit (0)