mirror of https://github.com/svaarala/duktape.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
398 lines
9.6 KiB
398 lines
9.6 KiB
#!/usr/bin/python
|
|
#
|
|
# Check various C source code policy rules and issue warnings for offenders
|
|
#
|
|
# Usage:
|
|
#
|
|
# $ python check_code_policy.py src/*.c
|
|
#
|
|
|
|
import os
|
|
import sys
|
|
import re
|
|
import optparse
|
|
|
|
class Problem:
|
|
filename = None
|
|
linenumber = None
|
|
line = None
|
|
reason = None
|
|
|
|
def __init__(self, filename, linenumber, line, reason):
|
|
self.filename = filename
|
|
self.linenumber = linenumber
|
|
self.line = line
|
|
self.reason = reason
|
|
|
|
|
|
re_debuglog_callsite = re.compile(r'^.*?(DUK_D+PRINT).*?$')
|
|
re_trailing_ws = re.compile(r'^.*?\s$')
|
|
re_only_ws = re.compile(r'^\s*$')
|
|
re_nonleading_tab = re.compile(r'^.*?[^\t]\t.*?$') # tabs are only used for indent
|
|
re_identifier = re.compile(r'[A-Za-z0-9_]+')
|
|
re_nonascii = re.compile(r'^.*?[\x80-\xff].*?$')
|
|
re_func_decl_or_def = re.compile(r'^(\w+)\s+(?:\w+\s+)*(\w+)\(.*?.*?$') # may not finish on same line
|
|
|
|
# These identifiers are wrapped in duk_config.h, and should only be used
|
|
# through the wrappers elsewhere.
|
|
rejected_plain_identifiers_list = [
|
|
# math classification
|
|
'fpclassify',
|
|
'signbit',
|
|
'isfinite',
|
|
'isnan',
|
|
'isinf',
|
|
'FP_NAN',
|
|
'FP_INFINITE',
|
|
'FP_ZERO',
|
|
'FP_SUBNORMAL',
|
|
'FP_NORMAL',
|
|
|
|
# math functions
|
|
'fabs',
|
|
'fmin',
|
|
'fmax',
|
|
'floor',
|
|
'ceil',
|
|
'fmod',
|
|
'pow',
|
|
'acos',
|
|
'asin',
|
|
'atan',
|
|
'atan2',
|
|
'sin',
|
|
'cos',
|
|
'tan',
|
|
'exp',
|
|
'log',
|
|
'sqrt',
|
|
|
|
# memory functions
|
|
'malloc',
|
|
'realloc',
|
|
'calloc',
|
|
'free',
|
|
'memcpy',
|
|
'memmove',
|
|
'memcmp',
|
|
'memset',
|
|
|
|
# string functions
|
|
'strlen',
|
|
'strcmp',
|
|
'strncmp',
|
|
'printf',
|
|
'fprintf',
|
|
'sprintf',
|
|
'_snprintf',
|
|
'snprintf',
|
|
'vsprintf',
|
|
'_vsnprintf',
|
|
'vsnprintf',
|
|
'sscanf',
|
|
'vsscanf',
|
|
|
|
# streams
|
|
'stdout',
|
|
'stderr',
|
|
'stdin',
|
|
|
|
# file ops
|
|
'fopen',
|
|
'fclose',
|
|
'fread',
|
|
'fwrite',
|
|
'fseek',
|
|
'ftell',
|
|
'fflush',
|
|
'fputc',
|
|
|
|
# misc
|
|
'abort',
|
|
'exit',
|
|
'setjmp',
|
|
'longjmp'
|
|
]
|
|
rejected_plain_identifiers = {}
|
|
for id in rejected_plain_identifiers_list:
|
|
rejected_plain_identifiers[id] = True
|
|
|
|
debuglog_wrappers = {
|
|
'DUK_DPRINT': 'DUK_D',
|
|
'DUK_DDPRINT': 'DUK_DD',
|
|
'DUK_DDDPRINT': 'DUK_DDD'
|
|
}
|
|
|
|
allowed_visibility_macros = [
|
|
'DUK_EXTERNAL_DECL',
|
|
'DUK_EXTERNAL',
|
|
'DUK_INTERNAL_DECL',
|
|
'DUK_INTERNAL',
|
|
'DUK_LOCAL_DECL',
|
|
'DUK_LOCAL'
|
|
]
|
|
|
|
problems = []
|
|
|
|
re_repl_c_comments = re.compile(r'/\*.*?\*/', re.DOTALL)
|
|
re_repl_cpp_comments = re.compile(r'//.*?\n', re.DOTALL)
|
|
re_repl_string_literals_dquot = re.compile(r'''\"(?:\\\"|[^\"])*\"''')
|
|
re_repl_string_literals_squot = re.compile(r'''\'(?:\\\'|[^\'])*\'''')
|
|
re_repl_expect_strings = re.compile(r'/\*===.*?===*?\*/', re.DOTALL)
|
|
re_not_newline = re.compile(r'[^\n]+', re.DOTALL)
|
|
|
|
def removeCommentsAndLiterals(data):
|
|
def repl_c(m):
|
|
tmp = re.sub(re_not_newline, '', m.group(0))
|
|
if tmp == '':
|
|
tmp = ' ' # avoid /**/
|
|
return '/*' + tmp + '*/'
|
|
def repl_cpp(m):
|
|
return '// removed\n'
|
|
def repl_dquot(m):
|
|
return '"' + ('.' * (len(m.group(0)) - 2)) + '"'
|
|
def repl_squot(m):
|
|
return "'" + ('.' * (len(m.group(0)) - 2)) + "'"
|
|
|
|
data = re.sub(re_repl_c_comments, repl_c, data)
|
|
data = re.sub(re_repl_cpp_comments, repl_cpp, data)
|
|
data = re.sub(re_repl_string_literals_dquot, repl_dquot, data)
|
|
data = re.sub(re_repl_string_literals_squot, repl_squot, data)
|
|
return data
|
|
|
|
def removeExpectStrings(data):
|
|
def repl(m):
|
|
tmp = re.sub(re_not_newline, '', m.group(0))
|
|
if tmp == '':
|
|
tmp = ' ' # avoid /*======*/
|
|
return '/*===' + tmp + '===*/'
|
|
|
|
data = re.sub(re_repl_expect_strings, repl, data)
|
|
return data
|
|
|
|
def checkDebugLogCalls(lines, idx, filename):
|
|
# Allowed debug log forms:
|
|
#
|
|
# DUK_D(DUK_DPRINT(...))
|
|
# DUK_DD(DUK_DDPRINT(...))
|
|
# DUK_DDD(DUK_DDDPRINT(...))
|
|
#
|
|
# The calls may span multiple lines, but the wrapper (DUK_D)
|
|
# and the log macro (DUK_DPRINT) must be on the same line.
|
|
|
|
line = lines[idx]
|
|
if 'DPRINT' not in line:
|
|
return
|
|
|
|
m = re_debuglog_callsite.match(line)
|
|
if m is None:
|
|
return
|
|
|
|
log_macro = m.group(1)
|
|
log_wrapper = debuglog_wrappers[log_macro]
|
|
if log_wrapper + '(' in line:
|
|
return
|
|
|
|
# exclude '#define DUK_DPRINT...' macros in duk_debug.h
|
|
if len(line) >= 1 and line[0] == '#':
|
|
return
|
|
|
|
# exclude a few comment lines in duk_debug.h
|
|
if len(line) >= 3 and line[0:3] == ' * ':
|
|
return
|
|
|
|
raise Exception('invalid debug log call form')
|
|
|
|
def checkTrailingWhitespace(lines, idx, filename):
|
|
line = lines[idx]
|
|
if len(line) > 0 and line[-1] == '\n':
|
|
line = line[:-1]
|
|
|
|
m = re_trailing_ws.match(line)
|
|
if m is None:
|
|
return
|
|
|
|
raise Exception('trailing whitespace')
|
|
|
|
def checkCarriageReturns(lines, idx, filename):
|
|
line = lines[idx]
|
|
if not '\x0d' in line:
|
|
return
|
|
|
|
raise Exception('carriage return')
|
|
|
|
def checkMixedIndent(lines, idx, filename):
|
|
line = lines[idx]
|
|
if not '\x20\x09' in line:
|
|
return
|
|
|
|
# Mixed tab/space are only allowed after non-whitespace characters
|
|
idx = line.index('\x20\x09')
|
|
tmp = line[0:idx]
|
|
m = re_only_ws.match(tmp)
|
|
if m is None:
|
|
return
|
|
|
|
raise Exception('mixed space/tab indent (idx %d)' % idx)
|
|
|
|
def checkNonLeadingTab(lines, idx, filename):
|
|
line = lines[idx]
|
|
m = re_nonleading_tab.match(line)
|
|
if m is None:
|
|
return
|
|
|
|
raise Exception('non-leading tab (idx %d)' % idx)
|
|
|
|
def checkFixme(lines, idx, filename):
|
|
line = lines[idx]
|
|
if not 'FIXME' in line:
|
|
return
|
|
|
|
raise Exception('FIXME on line')
|
|
|
|
def checkIdentifiers(lines, idx, filename):
|
|
line = lines[idx]
|
|
# XXX: this now executes for every line which is pointless
|
|
bn = os.path.basename(filename)
|
|
excludePlain = (bn[0:5] == 'test-')
|
|
|
|
for m in re.finditer(re_identifier, line):
|
|
if rejected_plain_identifiers.has_key(m.group(0)):
|
|
if not excludePlain:
|
|
raise Exception('invalid identifier %r (perhaps plain)' % m.group(0))
|
|
|
|
def checkNonAscii(lines, idx, filename):
|
|
line = lines[idx]
|
|
m = re_nonascii.match(line)
|
|
if m is None:
|
|
return
|
|
|
|
bn = os.path.basename(filename)
|
|
if bn == 'test-lex-utf8.js':
|
|
# this specific file is intentionally exempt
|
|
pass
|
|
else:
|
|
raise Exception('non-ascii character')
|
|
|
|
def checkNoSymbolVisibility(lines, idx, filename):
|
|
line = lines[idx]
|
|
|
|
# Workaround for DUK_ALWAYS_INLINE preceding a declaration
|
|
# (e.g. "DUK_ALWAYS_INLINE DUK_LOCAL ...")
|
|
if line.startswith('DUK_ALWAYS_INLINE '):
|
|
line = line[18:]
|
|
|
|
m = re_func_decl_or_def.match(line)
|
|
if m is None:
|
|
return
|
|
|
|
bn = os.path.basename(filename)
|
|
if not ((bn[-2:] == '.c' or bn[-2:] == '.h' or bn[-5:] == '.h.in') and bn[0:5] != 'test-'):
|
|
# Apply to only specific files in src/
|
|
return
|
|
|
|
if m.group(1) in allowed_visibility_macros and \
|
|
not ((m.group(1) != 'DUK_LOCAL' and m.group(1) != 'DUK_LOCAL_DECL') and 'duk__' in m.group(2)) and \
|
|
not ((m.group(1) == 'DUK_LOCAL' or m.group(1) == 'DUK_LOCAL_DECL') and 'duk__' not in m.group(2)):
|
|
return
|
|
|
|
# Previous line may contain the declaration (alone)
|
|
if idx > 0 and lines[idx - 1].strip() in allowed_visibility_macros:
|
|
return
|
|
|
|
# Special exceptions
|
|
# (None now)
|
|
|
|
raise Exception('missing symbol visibility macro')
|
|
|
|
def processFile(filename, checkersRaw, checkersNoComments, checkersNoExpectStrings):
|
|
f = open(filename, 'rb')
|
|
dataRaw = f.read()
|
|
f.close()
|
|
|
|
dataNoComments = removeCommentsAndLiterals(dataRaw) # no c/javascript comments, literals removed
|
|
dataNoExpectStrings = removeExpectStrings(dataRaw) # no testcase expect strings
|
|
|
|
linesRaw = dataRaw.split('\n')
|
|
linesNoComments = dataNoComments.split('\n')
|
|
linesNoExpectStrings = dataNoExpectStrings.split('\n')
|
|
|
|
def f(lines, checkers):
|
|
for linenumber in xrange(len(lines)):
|
|
for fun in checkers:
|
|
try:
|
|
fun(lines, linenumber, filename) # linenumber is zero-based here
|
|
except Exception as e:
|
|
problems.append(Problem(filename, linenumber + 1, lines[linenumber], str(e)))
|
|
|
|
f(linesRaw, checkersRaw)
|
|
f(linesNoComments, checkersNoComments)
|
|
f(linesNoExpectStrings, checkersNoExpectStrings)
|
|
|
|
# Last line should have a newline, and there should not be an empty line.
|
|
# The 'split' result will have one empty string as its last item in the
|
|
# expected case. For a single line file there will be two split results
|
|
# (the line itself, and an empty string).
|
|
|
|
if len(linesRaw) == 0 or \
|
|
len(linesRaw) == 1 and linesRaw[-1] != '' or \
|
|
len(linesRaw) >= 2 and linesRaw[-1] != '' or \
|
|
len(linesRaw) >= 2 and linesRaw[-1] == '' and linesRaw[-2] == '':
|
|
problems.append(Problem(filename, len(linesRaw), '(no line)', 'No newline on last line or empty line at end of file'))
|
|
|
|
# First line should not be empty (unless it's the only line, len(linesRaw)==2)
|
|
if len(linesRaw) > 2 and linesRaw[0] == '':
|
|
problems.append(Problem(filename, 1, '(no line)', 'First line is empty'))
|
|
|
|
def asciiOnly(x):
|
|
return re.sub(r'[\x80-\xff]', '#', x)
|
|
|
|
def main():
|
|
parser = optparse.OptionParser()
|
|
parser.add_option('--dump-vim-commands', dest='dump_vim_commands', default=False, help='Dump oneline vim command')
|
|
(opts, args) = parser.parse_args()
|
|
|
|
checkersRaw = []
|
|
checkersRaw.append(checkDebugLogCalls)
|
|
checkersRaw.append(checkCarriageReturns)
|
|
checkersRaw.append(checkFixme)
|
|
checkersRaw.append(checkNonAscii)
|
|
checkersRaw.append(checkNoSymbolVisibility)
|
|
|
|
checkersNoComments = []
|
|
checkersNoComments.append(checkIdentifiers)
|
|
|
|
checkersNoExpectStrings = []
|
|
checkersNoExpectStrings.append(checkTrailingWhitespace)
|
|
checkersNoExpectStrings.append(checkMixedIndent)
|
|
checkersNoExpectStrings.append(checkNonLeadingTab)
|
|
|
|
for filename in args:
|
|
processFile(filename, checkersRaw, checkersNoComments, checkersNoExpectStrings)
|
|
|
|
if len(problems) > 0:
|
|
for i in problems:
|
|
tmp = 'vim +' + str(i.linenumber)
|
|
while len(tmp) < 10:
|
|
tmp = tmp + ' '
|
|
tmp += ' ' + str(i.filename) + ' : ' + str(i.reason)
|
|
while len(tmp) < 80:
|
|
tmp = tmp + ' '
|
|
tmp += ' - ' + asciiOnly(i.line.strip())
|
|
print(tmp)
|
|
|
|
print '*** Total: %d problems' % len(problems)
|
|
|
|
if opts.dump_vim_commands:
|
|
cmds = []
|
|
for i in problems:
|
|
cmds.append('vim +' + str(i.linenumber) + ' "' + i.filename + '"')
|
|
print ''
|
|
print('; '.join(cmds))
|
|
|
|
sys.exit(1)
|
|
|
|
sys.exit(0)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|