257 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			257 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | #!/usr/bin/python -tt | ||
|  | # | ||
|  | # Copyright (C) 2005-2017 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.re_checks = \ | ||
|  | 			[ ( re.compile ("  "),		"multiple space instead of tab" ) | ||
|  | 			, ( re.compile ("\t "), 	"space after tab" ) | ||
|  | 			, ( re.compile ("[^ ];"),	"missing space before semi-colon" ) | ||
|  | 			, ( re.compile ("{[^\s}]"),	"missing space after open brace" ) | ||
|  | 			, ( re.compile ("[^{\s]}"),	"missing space before close brace" ) | ||
|  | 			, ( re.compile ("[ \t]+$"),	"contains trailing whitespace" ) | ||
|  | 
 | ||
|  | 			, ( re.compile (",[^\s\n]"),		"missing space after comma" ) | ||
|  | 			, ( re.compile (";[^\s]"), 	"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 ("\s(do|for|if|when)\s.*{$"),	"trailing open parenthesis at end of line" ) | ||
|  | 			, ( re.compile ("\( [^;]"),						"space after open parenthesis" ) | ||
|  | 			, ( re.compile ("[^;] \)"),						"space before close parenthesis" ) | ||
|  | 
 | ||
|  | 			# Open and close square brace. | ||
|  | 			, ( re.compile ("[^\s\(\]]\["),			"missing space before open square brace" ) | ||
|  | 			, ( re.compile ("\][^,\)\]\[\s\.-]"),	"missing space after 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" ) | ||
|  | 
 | ||
|  | 			# Parens around single case argument. | ||
|  | 			, ( re.compile ("\s+case\s+\([a-zA-Z0-9_]+\)\s+:"),	"parens around single case argument" ) | ||
|  | 
 | ||
|  | 			# Open curly at end of line. | ||
|  | 			, ( re.compile ("\)\s*{\s*$"),	"open curly brace at end of line" ) | ||
|  | 
 | ||
|  | 			# Pre and post increment/decrment. | ||
|  | 			, ( re.compile ("[^\(\[][+-]{2}[a-zA-Z0-9_]"),       "space after pre increment/decrement" ) | ||
|  | 			, ( re.compile ("[a-zA-Z0-9_][+-]{2}[^\)\,]]"),       "space before post increment/decrement" ) | ||
|  | 			] | ||
|  | 
 | ||
|  | 	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 | ||
|  | 		cfile = open (filename, "r") | ||
|  | 
 | ||
|  | 		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 regex checks. | ||
|  | 		for (check_re, msg) in self.re_checks: | ||
|  | 			if check_re.search (line): | ||
|  | 				self.error (msg) | ||
|  | 
 | ||
|  | 		if re.search ("[a-zA-Z0-9][<>!=^/&\|]{1,2}[a-zA-Z0-9]", line): | ||
|  | 			if not re.search (".*#include.*[a-zA-Z0-9]/[a-zA-Z]", 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) : %s" % (self.filename, self.line_num, msg)) | ||
|  | 		if self.debug: | ||
|  | 			print ("'" + self.orig_line + "'") | ||
|  | 		self.error_count += 1 | ||
|  | 
 | ||
|  | #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- | ||
|  | 
 | ||
|  | 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) |