summaryrefslogtreecommitdiff
path: root/tools/cstyle.py
blob: 7b7806bbb408d0216ab46f37a3d3054ceae46180 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#!/usr/bin/python -tt
#
# Copyright (C) 2005-2012 Erik de Castro Lopo <erikd@mega-nerd.com>
#
# Released under the 2 clause BSD license.

"""
This program checks C code for compliance to coding standards used in
libsndfile and other projects I run.
"""

import re
import sys


class Preprocessor:
	"""
	Preprocess lines of C code to make it easier for the CStyleChecker class to
	test for correctness. Preprocessing works on a single line at a time but
	maintains state between consecutive lines so it can preprocessess multi-line
	comments.
	Preprocessing involves:
	  - Strip C++ style comments from a line.
	  - Strip C comments from a series of lines. When a C comment starts and
	    ends on the same line it will be replaced with 'comment'.
	  - Replace arbitrary C strings with the zero length string.
	  - Replace '#define f(x)' with '#define f (c)' (The C #define requires that
	    there be no space between defined macro name and the open paren of the
	    argument list).
	Used by the CStyleChecker class.
	"""
	def __init__ (self):
		self.comment_nest = 0
		self.leading_space_re = re.compile ('^(\t+| )')
		self.trailing_space_re = re.compile ('(\t+| )$')
		self.define_hack_re = re.compile ("(#\s*define\s+[a-zA-Z0-9_]+)\(")

	def comment_nesting (self):
		"""
		Return the currect comment nesting. At the start and end of the file,
		this value should be zero. Inside C comments it should be 1 or
		(possibly) more.
		"""
		return self.comment_nest

	def __call__ (self, line):
		"""
		Strip the provided line of C and C++ comments. Stripping of multi-line
		C comments works as expected.
		"""

		line = self.define_hack_re.sub (r'\1 (', line)

		line = self.process_strings (line)

		# Strip C++ style comments.
		if self.comment_nest == 0:
			line = re.sub ("( |\t*)//.*", '', line)

		# Strip C style comments.
		open_comment = line.find ('/*')
		close_comment = line.find ('*/')

		if self.comment_nest > 0 and close_comment < 0:
			# Inside a comment block that does not close on this line.
			return ""

		if open_comment >= 0 and close_comment < 0:
			# A comment begins on this line but doesn't close on this line.
			self.comment_nest += 1
			return self.trailing_space_re.sub ('', line [:open_comment])

		if open_comment < 0 and close_comment >= 0:
			# Currently open comment ends on this line.
			self.comment_nest -= 1
			return self.trailing_space_re.sub ('', line [close_comment + 2:])

		if open_comment >= 0 and close_comment > 0 and self.comment_nest == 0:
			# Comment begins and ends on this line. Replace it with 'comment'
			# so we don't need to check whitespace before and after the comment
			# we're removing.
			newline = line [:open_comment] + "comment" + line [close_comment + 2:]
			return self.__call__ (newline)

		return line

	def process_strings (self, line):
		"""
		Given a line of C code, return a string where all literal C strings have
		been replaced with the empty string literal "".
		"""
		for k in range (0, len (line)):
			if line [k] == '"':
				start = k
				for k in range (start + 1, len (line)):
					if line [k] == '"' and line [k - 1] != '\\':
						return line [:start + 1] + '"' + self.process_strings (line [k + 1:])
		return line


class CStyleChecker:
	"""
	A class for checking the whitespace and layout of a C code.
	"""
	def __init__ (self, debug):
		self.debug = debug
		self.filename = None
		self.error_count = 0
		self.line_num = 1
		self.orig_line = ''
		self.trailing_newline_re = re.compile ('[\r\n]+$')
		self.indent_re = re.compile ("^\s*")
		self.last_line_indent = ""
		self.last_line_indent_curly = False
		self.error_checks = \
                        [ ( re.compile ("^ "),          "leading space as indentation instead of tab - use tabs to indent, spaces to align" )
                        ]                                                                                                                                       
		self.warning_checks = \
                        [   ( re.compile ("{[^\s]"),      "missing space after open brace" )
                          , ( re.compile ("[^\s]}"),      "missing space before close brace" )
                          , ( re.compile ("^[ \t]+$"),     "empty line contains whitespace" )
                          , ( re.compile ("[^\s][ \t]+$"),     "contains trailing whitespace" )

                          , ( re.compile (",[^\s\n]"),            "missing space after comma" )
                          , ( re.compile (";[a-zA-Z0-9]"),        "missing space after semi-colon" )
                          , ( re.compile ("=[^\s\"'=]"),          "missing space after assignment" )
                          
                          # Open and close parenthesis.
                          , ( re.compile ("[^_\s\(\[\*&']\("),             "missing space before open parenthesis" )
                          , ( re.compile ("\)(-[^>]|[^;,'\s\n\)\]-])"),    "missing space after close parenthesis" )
                          , ( re.compile ("\( [^;]"),                     "space after open parenthesis" )
                          , ( re.compile ("[^;] \)"),                     "space before close parenthesis" )

                          # Open and close square brace.
                          , ( re.compile ("\[ "),                                 "space after open square brace" )
                          , ( re.compile (" \]"),                                 "space before close square brace" )

                          # Space around operators.
                          , ( re.compile ("[^\s][\*/%+-][=][^\s]"),               "missing space around opassign" )
                          , ( re.compile ("[^\s][<>!=^/][=]{1,2}[^\s]"),  "missing space around comparison" )

                          # Parens around single argument to return.
                          , ( re.compile ("\s+return\s+\([a-zA-Z0-9_]+\)\s+;"),   "parens around return value" )
                        ]                                                                                                                                       

                

	def get_error_count (self):
		"""
		Return the current error count for this CStyleChecker object.
		"""
		return self.error_count

	def check_files (self, files):
		"""
		Run the style checker on all the specified files.
		"""
		for filename in files:
			self.check_file (filename)

	def check_file (self, filename):
		"""
		Run the style checker on the specified file.
		"""
		self.filename = filename
		try:
			cfile = open (filename, "r")
		except IOError as e:
			return

		self.line_num = 1

		preprocess = Preprocessor ()
		while 1:
			line = cfile.readline ()
			if not line:
				break

			line = self.trailing_newline_re.sub ('', line)
			self.orig_line = line

			self.line_checks (preprocess (line))

			self.line_num += 1

		cfile.close ()
		self.filename = None

		# Check for errors finding comments.
		if preprocess.comment_nesting () != 0:
			print ("Weird, comments nested incorrectly.")
			sys.exit (1)

		return

	def line_checks (self, line):
		"""
		Run the style checker on provided line of text, but within the context
		of how the line fits within the file.
		"""

		indent = len (self.indent_re.search (line).group ())
		if re.search ("^\s+}", line):
			if not self.last_line_indent_curly and indent != self.last_line_indent:
				None	# self.error ("bad indent on close curly brace")
			self.last_line_indent_curly = True
		else:
			self.last_line_indent_curly = False

		# Now all the stylistic warnings regex checks.
		for (check_re, msg) in self.warning_checks:
			if check_re.search (line):
				self.warning (msg)

                # Now all the stylistic error regex checks.
		for (check_re, msg) in self.error_checks:
			if check_re.search (line):
				self.error (msg)

                                
		if re.search ("[a-zA-Z0-9_][<>!=^/&\|]{1,2}[a-zA-Z0-9_]", line):
                        # ignore #include <foo.h> and C++ templates with indirection/pointer/reference operators
			if not re.search (".*#include.*[a-zA-Z0-9]/[a-zA-Z]", line) and not re.search ("[a-zA-Z0-9_]>[&\*]*\s", line):
				self.error ("missing space around operator")

		self.last_line_indent = indent
		return

	def error (self, msg):
		"""
		Print an error message and increment the error count.
		"""
		print ("%s (%d) : STYLE ERROR %s" % (self.filename, self.line_num, msg))
		if self.debug:
			print ("'" + self.orig_line + "'")
		self.error_count += 1

	def warning (self, msg):
		"""
		Print a warning message and increment the error count.
		"""
		print ("%s (%d) : STYLE WARNING %s" % (self.filename, self.line_num, msg))
		if self.debug:
			print ("'" + self.orig_line + "'")
                
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

if len (sys.argv) < 1:
	print ("Usage : yada yada")
	sys.exit (1)

# Create a new CStyleChecker object
if sys.argv [1] == '-d' or sys.argv [1] == '--debug':
	cstyle = CStyleChecker (True)
	cstyle.check_files (sys.argv [2:])
else:
	cstyle = CStyleChecker (False)
	cstyle.check_files (sys.argv [1:])


if cstyle.get_error_count ():
	sys.exit (1)

sys.exit (0)