From d6a6444b90576c5d8149c74725eae3f4c7737571 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Sat, 9 May 2015 13:40:35 -0400 Subject: C/C++ style checking script from erik de castro-lopo, for use in client-side pre-commit hook --- tools/cstyle.py | 245 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 tools/cstyle.py (limited to 'tools') diff --git a/tools/cstyle.py b/tools/cstyle.py new file mode 100644 index 0000000000..f78c089724 --- /dev/null +++ b/tools/cstyle.py @@ -0,0 +1,245 @@ +#!/usr/bin/python -tt +# +# Copyright (C) 2005-2012 Erik de Castro Lopo +# +# 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 ("^ "), "leading space as indentation instead of tab - use tabs to indent, spaces to align" ) + , ( 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 (";[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 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) -- cgit v1.2.3