• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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