Add --suppress-rules argument for misra.py (#1259)

* Added ignore list

- Added ignore list
- Use argparse to parse arguments
- Source formatting

* misra.py changed argument name

- Changed ` --ignore-rules` to ` --suppress-rules`

* Resolved VERIFY option in misra.py

Resolved broken broken logic in commit to add argparse.

* edit suppression list

Modified names
Hide some arguments

* command line help formatting

* Revert removal of --no-summary argument

* missing text rules no longer reported

* VERIFY scope modified to global
This commit is contained in:
Kyle Chisholm 2018-05-24 00:31:20 -04:00 committed by Daniel Marjamäki
parent 9f47d04af6
commit 6ce3571bdd
1 changed files with 158 additions and 102 deletions

View File

@ -19,25 +19,38 @@ import cppcheckdata
import sys import sys
import re import re
import os import os
import tempfile import argparse
import subprocess
ruleTexts = {} ruleTexts = {}
suppressRules = {}
typeBits = {
'CHAR': None,
'SHORT': None,
'INT': None,
'LONG': None,
'LONG_LONG': None,
'POINTER': None
}
VERIFY = False VERIFY = False
SHOW_SUMMARY = True
QUIET = False QUIET = False
SHOW_SUMMARY = True
VERIFY_EXPECTED = [] VERIFY_EXPECTED = []
VERIFY_ACTUAL = [] VERIFY_ACTUAL = []
VIOLATIONS = [] VIOLATIONS = []
def printStatus(*args, **kwargs): def printStatus(*args, **kwargs):
if not QUIET: if not QUIET:
print(*args, **kwargs) print(*args, **kwargs)
def reportError(location, num1, num2): def reportError(location, num1, num2):
if VERIFY: if VERIFY:
VERIFY_ACTUAL.append(str(location.linenr) + ':' + str(num1) + '.' + str(num2)) VERIFY_ACTUAL.append(str(location.linenr) + ':' + str(num1) + '.' + str(num2))
elif num1 in suppressRules and num2 in suppressRules[num1]:
# ignore error
return
else: else:
num = num1 * 100 + num2 num = num1 * 100 + num2
id = 'misra-c2012-' + str(num1) + '.' + str(num2) id = 'misra-c2012-' + str(num1) + '.' + str(num2)
@ -47,11 +60,11 @@ def reportError(location, num1, num2):
errmsg = 'misra violation (use --rule-texts=<file> to get proper output) [' + id + ']' errmsg = 'misra violation (use --rule-texts=<file> to get proper output) [' + id + ']'
else: else:
return return
errmsg = '[' + location.file + ':' + str(location.linenr) + '] (style): ' + errmsg + '\n' sys.stderr.write('[' + location.file + ':' + str(location.linenr) + '] (style): ' + errmsg + '\n')
sys.stderr.write(errmsg)
VIOLATIONS.append(errmsg) VIOLATIONS.append(errmsg)
def simpleMatch(token, pattern): def simpleMatch(token, pattern):
for p in pattern.split(' '): for p in pattern.split(' '):
if not token or token.str != p: if not token or token.str != p:
@ -59,6 +72,7 @@ def simpleMatch(token, pattern):
token = token.next token = token.next
return True return True
def rawlink(rawtoken): def rawlink(rawtoken):
if rawtoken.str == '}': if rawtoken.str == '}':
indent = 0 indent = 0
@ -74,6 +88,7 @@ def rawlink(rawtoken):
rawtoken = None rawtoken = None
return rawtoken return rawtoken
KEYWORDS = { KEYWORDS = {
'auto', 'auto',
'break', 'break',
@ -178,15 +193,15 @@ def bitsOfEssentialType(expr):
if type is None: if type is None:
return 0 return 0
if type == 'char': if type == 'char':
return CHAR_BIT return typeBits['CHAR']
if type == 'short': if type == 'short':
return SHORT_BIT return typeBits['SHORT']
if type == 'int': if type == 'int':
return INT_BIT return typeBits['INT']
if type == 'long': if type == 'long':
return LONG_BIT return typeBits['LONG']
if type == 'long long': if type == 'long long':
return LONG_LONG_BIT return typeBits['LONG_LONG']
return 0 return 0
@ -356,7 +371,7 @@ def findRawLink(token):
if indent <= 1: if indent <= 1:
return token return token
indent = indent - 1 indent = indent - 1
if forward == True: if forward is True:
token = token.next token = token.next
else: else:
token = token.previous token = token.previous
@ -364,6 +379,7 @@ def findRawLink(token):
# raw link not found # raw link not found
return None return None
def numberOfParentheses(tok1, tok2): def numberOfParentheses(tok1, tok2):
while tok1 and tok1 != tok2: while tok1 and tok1 != tok2:
if tok1.str == '(' or tok1.str == ')': if tok1.str == '(' or tok1.str == ')':
@ -399,7 +415,8 @@ def getArgumentsRecursive(tok, arguments):
getArgumentsRecursive(tok.astOperand1, arguments) getArgumentsRecursive(tok.astOperand1, arguments)
getArgumentsRecursive(tok.astOperand2, arguments) getArgumentsRecursive(tok.astOperand2, arguments)
else: else:
arguments.append(tok); arguments.append(tok)
def getArguments(ftok): def getArguments(ftok):
arguments = [] arguments = []
@ -410,9 +427,11 @@ def getArguments(ftok):
def isHexDigit(c): def isHexDigit(c):
return (c >= '0' and c <= '9') or (c >= 'a' and c <= 'f') or (c >= 'A' and c >= 'F') return (c >= '0' and c <= '9') or (c >= 'a' and c <= 'f') or (c >= 'A' and c >= 'F')
def isOctalDigit(c): def isOctalDigit(c):
return (c >= '0' and c <= '7') return (c >= '0' and c <= '7')
def isNoReturnScope(tok): def isNoReturnScope(tok):
if tok is None or tok.str != '}': if tok is None or tok.str != '}':
return False return False
@ -429,12 +448,14 @@ def isNoReturnScope(tok):
return True return True
return False return False
def misra_3_1(rawTokens): def misra_3_1(rawTokens):
for token in rawTokens: for token in rawTokens:
if token.str.startswith('/*') or token.str.startswith('//'): if token.str.startswith('/*') or token.str.startswith('//'):
if '//' in token.str[2:] or '/*' in token.str[2:]: if '//' in token.str[2:] or '/*' in token.str[2:]:
reportError(token, 3, 1) reportError(token, 3, 1)
def misra_4_1(rawTokens): def misra_4_1(rawTokens):
for token in rawTokens: for token in rawTokens:
if token.str[0] != '"': if token.str[0] != '"':
@ -445,21 +466,21 @@ def misra_4_1(rawTokens):
pos = pos + 1 pos = pos + 1
if token.str[pos1] != '\\': if token.str[pos1] != '\\':
continue continue
if token.str[pos1+1] == '\\': if token.str[pos1 + 1] == '\\':
pos = pos1 + 2 pos = pos1 + 2
continue continue
if token.str[pos1+1] == 'x': if token.str[pos1 + 1] == 'x':
if not isHexDigit(token.str[pos1+2]): if not isHexDigit(token.str[pos1 + 2]):
reportError(token, 4, 1) reportError(token, 4, 1)
continue continue
if not isHexDigit(token.str[pos1+3]): if not isHexDigit(token.str[pos1 + 3]):
reportError(token, 4, 1) reportError(token, 4, 1)
continue continue
elif isOctalDigit(token.str[pos1+1]): elif isOctalDigit(token.str[pos1 + 1]):
if not isOctalDigit(token.str[pos1+2]): if not isOctalDigit(token.str[pos1 + 2]):
reportError(token, 4, 1) reportError(token, 4, 1)
continue continue
if not isOctalDigit(token.str[pos1+2]): if not isOctalDigit(token.str[pos1 + 2]):
reportError(token, 4, 1) reportError(token, 4, 1)
continue continue
else: else:
@ -489,6 +510,7 @@ def misra_5_1(data):
else: else:
reportError(variable2.nameToken, 5, 1) reportError(variable2.nameToken, 5, 1)
def misra_5_2(data): def misra_5_2(data):
scopeVars = {} scopeVars = {}
for var in data.variables: for var in data.variables:
@ -534,7 +556,6 @@ def misra_5_2(data):
reportError(scopename2.bodyStart, 5, 2) reportError(scopename2.bodyStart, 5, 2)
def misra_5_3(data): def misra_5_3(data):
enum = [] enum = []
scopeVars = {} scopeVars = {}
@ -587,6 +608,7 @@ def misra_5_3(data):
if (scope.className and scope.className[:31] == e[:31]): if (scope.className and scope.className[:31] == e[:31]):
reportError(scope.bodyStart, 5, 3) reportError(scope.bodyStart, 5, 3)
def misra_5_4(data): def misra_5_4(data):
macro = {} macro = {}
compile_name = re.compile(r'#define ([a-zA-Z0-9_]+)') compile_name = re.compile(r'#define ([a-zA-Z0-9_]+)')
@ -986,7 +1008,8 @@ def misra_12_2(data):
def misra_12_3(data): def misra_12_3(data):
for token in data.tokenlist: for token in data.tokenlist:
if token.str != ',' or token.scope.type == 'Enum' or token.scope.type == 'Class' or token.scope.type == 'Global': if token.str != ',' or token.scope.type == 'Enum' or \
token.scope.type == 'Class' or token.scope.type == 'Global':
continue continue
if token.astParent and token.astParent.str in ['(', ',', '{']: if token.astParent and token.astParent.str in ['(', ',', '{']:
continue continue
@ -994,9 +1017,9 @@ def misra_12_3(data):
def misra_12_4(data): def misra_12_4(data):
if INT_BIT == 16: if typeBits['INT'] == 16:
max_uint = 0xffff max_uint = 0xffff
elif INT_BIT == 32: elif typeBits['INT'] == 32:
max_uint = 0xffffffff max_uint = 0xffffffff
else: else:
return return
@ -1392,6 +1415,7 @@ def misra_20_5(data):
if directive.str.startswith('#undef '): if directive.str.startswith('#undef '):
reportError(directive, 20, 5) reportError(directive, 20, 5)
def misra_20_13(data): def misra_20_13(data):
for directive in data.directives: for directive in data.directives:
dir = directive.str dir = directive.str
@ -1399,9 +1423,11 @@ def misra_20_13(data):
dir = dir[:dir.find(' ')] dir = dir[:dir.find(' ')]
if dir.find('(') > 0: if dir.find('(') > 0:
dir = dir[:dir.find('(')] dir = dir[:dir.find('(')]
if dir not in ['#define', '#elif', '#else', '#endif', '#error', '#if', '#ifdef', '#ifndef', '#include', '#pragma', '#undef', '#warning']: if dir not in ['#define', '#elif', '#else', '#endif', '#error', '#if', '#ifdef', '#ifndef', '#include',
'#pragma', '#undef', '#warning']:
reportError(directive, 20, 13) reportError(directive, 20, 13)
def misra_20_14(data): def misra_20_14(data):
# stack for #if blocks. contains the #if directive until the corresponding #endif is seen. # stack for #if blocks. contains the #if directive until the corresponding #endif is seen.
# the size increases when there are inner #if directives. # the size increases when there are inner #if directives.
@ -1410,7 +1436,7 @@ def misra_20_14(data):
if directive.str.startswith('#if ') or directive.str.startswith('#ifdef ') or directive.str.startswith('#ifndef '): if directive.str.startswith('#if ') or directive.str.startswith('#ifdef ') or directive.str.startswith('#ifndef '):
ifStack.append(directive) ifStack.append(directive)
elif directive.str == '#else' or directive.str.startswith('#elif '): elif directive.str == '#else' or directive.str.startswith('#elif '):
if len(ifStack)==0: if len(ifStack) == 0:
reportError(directive, 20, 14) reportError(directive, 20, 14)
ifStack.append(directive) ifStack.append(directive)
elif directive.file != ifStack[-1].file: elif directive.file != ifStack[-1].file:
@ -1422,6 +1448,7 @@ def misra_20_14(data):
reportError(directive, 20, 14) reportError(directive, 20, 14)
ifStack.pop() ifStack.pop()
def misra_21_3(data): def misra_21_3(data):
for token in data.tokenlist: for token in data.tokenlist:
if isFunctionCall(token) and (token.astOperand1.str in {'malloc', 'calloc', 'realloc', 'free'}): if isFunctionCall(token) and (token.astOperand1.str in {'malloc', 'calloc', 'realloc', 'free'}):
@ -1483,6 +1510,26 @@ def misra_21_11(data):
reportError(directive, 21, 11) reportError(directive, 21, 11)
def setSuppressionList(suppressionlist):
num1 = 0
num2 = 0
global suppressRules
rule_pattern = re.compile(r'([0-9]+).([0-9]+)')
strlist = suppressionlist.split(",")
# build ignore list
suppressRules = {}
for item in strlist:
res = rule_pattern.match(item)
if res:
num1 = int(res.group(1))
num2 = int(res.group(2))
if num1 in suppressRules:
suppressRules[num1][num2] = True
else:
suppressRules[num1] = {num2: True}
def loadRuleTexts(filename): def loadRuleTexts(filename):
num1 = 0 num1 = 0
num2 = 0 num2 = 0
@ -1526,37 +1573,6 @@ def loadRuleTexts(filename):
ruleTexts[num] = ruleTexts[num] + ' ' + line ruleTexts[num] = ruleTexts[num] + ' ' + line
continue continue
if len(sys.argv) == 1:
print("""
Syntax: misra.py [OPTIONS] <dumpfiles>
OPTIONS:
--rule-texts=<file> Load rule texts from plain text file.
If you have the tool 'pdftotext' you might be able
to generate this textfile with such command:
$ pdftotext MISRA_C_2012.pdf MISRA_C_2012.txt
Otherwise you can more or less copy/paste the chapter
Appendix A Summary of guidelines
from the MISRA pdf. You can buy the MISRA pdf from
http://www.misra.org.uk/
Format:
<..arbitrary text..>
Appendix A Summary of guidelines
Rule 1.1
Rule text for 1.1
Rule 1.2
Rule text for 1.2
<...>
--quiet Only print something when there is an error
""")
sys.exit(1)
def generateTable(): def generateTable():
numberOfRules = {} numberOfRules = {}
@ -1593,14 +1609,15 @@ def generateTable():
addon.append(res.group(1) + '.' + res.group(2)) addon.append(res.group(1) + '.' + res.group(2))
# rules handled by cppcheck # rules handled by cppcheck
cppcheck = ['1.3', '2.1', '2.2', '2.4', '2.6', '8.3', '12.2', '13.2', '13.6', '17.5', '18.1', '18.6', '20.6', '22.1', '22.2', '22.4', '22.6'] cppcheck = ['1.3', '2.1', '2.2', '2.4', '2.6', '8.3', '12.2', '13.2', '13.6', '17.5', '18.1', '18.6',
'20.6', '22.1', '22.2', '22.4', '22.6']
# rules that can be checked with compilers # rules that can be checked with compilers
compiler = ['1.1', '1.2'] # compiler = ['1.1', '1.2']
# print table # print table
for i1 in range(1,23): for i1 in range(1, 23):
for i2 in range(1,numberOfRules[i1]+1): for i2 in range(1, numberOfRules[i1] + 1):
num = str(i1) + '.' + str(i2) num = str(i1) + '.' + str(i2)
s = '' s = ''
if num in addon: if num in addon:
@ -1611,44 +1628,19 @@ def generateTable():
print(num[:8] + s) print(num[:8] + s)
sys.exit(1) sys.exit(1)
for arg in sys.argv[1:]:
if arg == '-verify':
VERIFY = True
elif arg.startswith('--rule-texts='):
filename = arg[13:]
if not os.path.isfile(filename):
print('Fatal error: file is not found: ' + filename)
sys.exit(1)
loadRuleTexts(filename)
elif ".dump" in arg:
continue
elif arg == "-generate-table":
generateTable()
elif arg == "--no-summary":
SHOW_SUMMARY = False
elif arg == "--quiet":
QUIET = True
else:
print('Fatal error: unhandled argument ' + arg)
sys.exit(1)
exitCode = 0 def parseDump(dumpfile):
for arg in sys.argv[1:]:
if not arg.endswith('.dump'):
continue
data = cppcheckdata.parsedump(arg) data = cppcheckdata.parsedump(dumpfile)
CHAR_BIT = data.platform.char_bit typeBits['CHAR'] = data.platform.char_bit
SHORT_BIT = data.platform.short_bit typeBits['SHORT'] = data.platform.short_bit
INT_BIT = data.platform.int_bit typeBits['INT'] = data.platform.int_bit
LONG_BIT = data.platform.long_bit typeBits['LONG'] = data.platform.long_bit
LONG_LONG_BIT = data.platform.long_long_bit typeBits['LONG_LONG'] = data.platform.long_long_bit
POINTER_BIT = data.platform.pointer_bit typeBits['POINTER'] = data.platform.pointer_bit
if VERIFY: if VERIFY:
VERIFY_ACTUAL = []
VERIFY_EXPECTED = []
for tok in data.rawTokens: for tok in data.rawTokens:
if tok.str.startswith('//') and 'TODO' not in tok.str: if tok.str.startswith('//') and 'TODO' not in tok.str:
compiled = re.compile(r'[0-9]+\.[0-9]+') compiled = re.compile(r'[0-9]+\.[0-9]+')
@ -1656,14 +1648,14 @@ for arg in sys.argv[1:]:
if compiled.match(word): if compiled.match(word):
VERIFY_EXPECTED.append(str(tok.linenr) + ':' + word) VERIFY_EXPECTED.append(str(tok.linenr) + ':' + word)
else: else:
printStatus('Checking ' + arg + '...') printStatus('Checking ' + dumpfile + '...')
cfgNumber = 0 cfgNumber = 0
for cfg in data.configurations: for cfg in data.configurations:
cfgNumber = cfgNumber + 1 cfgNumber = cfgNumber + 1
if len(data.configurations) > 1: if len(data.configurations) > 1:
printStatus('Checking ' + arg + ', config "' + cfg.name + '"...') printStatus('Checking ' + dumpfile + ', config "' + cfg.name + '"...')
if cfgNumber == 1: if cfgNumber == 1:
misra_3_1(data.rawTokens) misra_3_1(data.rawTokens)
@ -1747,8 +1739,9 @@ for arg in sys.argv[1:]:
misra_21_10(cfg) misra_21_10(cfg)
misra_21_11(cfg) misra_21_11(cfg)
# 22.4 is already covered by Cppcheck writeReadOnlyFile # 22.4 is already covered by Cppcheck writeReadOnlyFile
exitCode = 0
if VERIFY: if VERIFY:
exitCode = 0
for expected in VERIFY_EXPECTED: for expected in VERIFY_EXPECTED:
if expected not in VERIFY_ACTUAL: if expected not in VERIFY_ACTUAL:
print('Expected but not seen: ' + expected) print('Expected but not seen: ' + expected)
@ -1757,11 +1750,74 @@ for arg in sys.argv[1:]:
if actual not in VERIFY_EXPECTED: if actual not in VERIFY_EXPECTED:
print('Not expected: ' + actual) print('Not expected: ' + actual)
exitCode = 1 exitCode = 1
else:
if len(VIOLATIONS) > 0:
if SHOW_SUMMARY:
print("\nRule violations found: %d\n" % (len(VIOLATIONS)))
exitCode = 1
if not VERIFY: sys.exit(exitCode)
if len(VIOLATIONS) > 0:
if SHOW_SUMMARY:
print("\nRule violations found: %d\n"%(len(VIOLATIONS)))
exitCode = 1
sys.exit(exitCode)
RULE_TEXTS_HELP = '''Path to text file of MISRA rules
If you have the tool 'pdftotext' you might be able
to generate this textfile with such command:
pdftotext MISRA_C_2012.pdf MISRA_C_2012.txt
Otherwise you can more or less copy/paste the chapter
Appendix A Summary of guidelines
from the MISRA pdf. You can buy the MISRA pdf from
http://www.misra.org.uk/
Format:
<..arbitrary text..>
Appendix A Summary of guidelines
Rule 1.1
Rule text for 1.1
Rule 1.2
Rule text for 1.2
<...>
'''
SUPPRESS_RULES_HELP = '''MISRA rules to suppress (comma-separated)
For example, if you'd like to suppress rules 15.1, 11.3,
and 20.13, run:
python misra.py --suppress-rules 15.1,11.3,20.13 ...
'''
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument("--rule-texts", type=str, help=RULE_TEXTS_HELP)
parser.add_argument("--suppress-rules", type=str, help=SUPPRESS_RULES_HELP)
parser.add_argument("--quiet", help="Only print something when there is an error", action="store_true")
parser.add_argument("--no-summary", help="Hide summary of violations", action="store_true")
parser.add_argument("-verify", help=argparse.SUPPRESS, action="store_true")
parser.add_argument("-generate-table", help=argparse.SUPPRESS, action="store_true")
parser.add_argument("file", help="Path of dump file from cppcheck")
args = parser.parse_args()
if args.generate_table:
generateTable()
else:
if args.verify:
VERIFY = True
if args.rule_texts:
filename = os.path.normpath(args.rule_texts)
if not os.path.isfile(filename):
print('Fatal error: file is not found: ' + filename)
sys.exit(1)
loadRuleTexts(filename)
if args.suppress_rules:
setSuppressionList(args.suppress_rules)
if args.quiet:
QUIET = True
if args.no_summary:
SHOW_SUMMARY = False
if args.file:
parseDump(args.file)