#/usr/bin/python # # MISRA C 2012 checkers # # Example usage of this addon (scan a sourcefile main.cpp) # cppcheck --dump main.cpp # python misra.py main.cpp.dump # # Limitations: This addon is released as open source. Rule texts can't be freely # distributed. https://www.misra.org.uk/forum/viewtopic.php?f=56&t=1189 # # import cppcheckdata import sys import re VERIFY = False VERIFY_EXPECTED = [] VERIFY_ACTUAL = [] def reportError(token, num1, num2): if VERIFY: VERIFY_ACTUAL.append(str(token.linenr) + ':' + str(num1) + '.' + str(num2)) else: sys.stderr.write('[' + token.file + ':' + str(token.linenr) + '] misra ' + str(num1) + '.' + str(num2) + ' violation\n') # Platform # TODO get this from dump CHAR_BITS = 8 SHORT_BITS = 16 INT_BITS = 32 def getEssentialType(expr): if not expr: return None if expr.variable: typeToken = expr.variable.typeStartToken while typeToken and typeToken.isName: if typeToken.str in ['char', 'short', 'int', 'long', 'float', 'double']: return typeToken.str typeToken = typeToken.next return None def bitsOfEssentialType(expr): type = getEssentialType(expr) if type is None: return 0 # TODO get --platform type sizes if type == 'char': return CHAR_BITS if type == 'short': return SHORT_BITS if type == 'int': return INT_BITS return 0 def isFunctionCall(expr): if not expr: return False if expr.str != '(' or not expr.astOperand1: return False if expr.astOperand1 != expr.previous: return False if expr.astOperand1.str in ['sizeof', 'if', 'switch', 'while']: return False return True def countSideEffects(expr): if not expr or expr.str in [',', ';']: return 0 ret = 0 if expr.str in ['++', '--', '=']: ret = 1 elif isFunctionCall(expr): ret = 1 return ret + countSideEffects(expr.astOperand1) + countSideEffects(expr.astOperand2) def getForLoopExpressions(forToken): if not forToken or forToken.str != 'for': return None lpar = forToken.next if not lpar or lpar.str != '(': return None if not lpar.astOperand2 or lpar.astOperand2.str != ';': return None if not lpar.astOperand2.astOperand2 or lpar.astOperand2.astOperand2.str != ';': return None return [lpar.astOperand2.astOperand1, lpar.astOperand2.astOperand2.astOperand1, lpar.astOperand2.astOperand2.astOperand2] def hasFloatComparison(expr): if not expr: return False if expr.isLogicalOp: return hasFloatComparison(expr.astOperand1) or hasFloatComparison(expr.astOperand2) if expr.isComparisonOp: # TODO: Use ValueType return cppcheckdata.astIsFloat(expr.astOperand1) or cppcheckdata.astIsFloat(expr.astOperand2) return False def hasSideEffectsRecursive(expr): if not expr: return False if expr.str in ['++', '--', '=']: return True # Todo: Check function calls return hasSideEffectsRecursive(expr.astOperand1) or hasSideEffectsRecursive(expr.astOperand2) def isBoolExpression(expr): return expr and expr.str in ['!', '==', '!=', '<', '<=', '>', '>=', '&&', '||'] def isConstantExpression(expr): if expr.isNumber: return True if expr.isName: return False if expr.astOperand1 and not isConstantExpression(expr.astOperand1): return False if expr.astOperand2 and not isConstantExpression(expr.astOperand2): return False return True def isUnsignedInt(expr): # TODO this function is very incomplete. use ValueType? if not expr: return False if expr.isNumber: return expr.str.find('u')>0 or expr.str.find('U')>0 if expr.str in ['+','-','*','/','%']: return isUnsignedInt(expr.astOperand1) or isUnsignedInt(expr.astOperand2) return False def getPrecedence(expr): if not expr: return 16 if not expr.astOperand1 or not expr.astOperand2: return 16 if expr.str in ['*', '/', '%']: return 12 if expr.str in ['+', '-']: return 11 if expr.str in ['<<', '>>']: return 10 if expr.str in ['<', '>', '<=', '>=']: return 9 if expr.str in ['==', '!=']: return 8 if expr.str == '&': return 7 if expr.str == '^': return 6 if expr.str == '|': return 5 if expr.str == '&&': return 4 if expr.str == '||': return 3 if expr.str in ['?',':']: return 2 if expr.isAssignmentOp: return 1 if expr.str == ',': return 0 return -1 def noParentheses(tok1, tok2): while tok1 and tok1 != tok2: if tok1.str == '(' or tok1.str == ')': return False tok1 = tok1.next return tok1 == tok2 def findGotoLabel(gotoToken): label = gotoToken.next.str tok = gotoToken.next.next while tok: if tok.str == '}' and tok.scope.type == 'Function': break if tok.str == label and tok.next.str == ':': return tok tok = tok.next return None def misra_5_1(data): for token in data.tokenlist: if token.isName and len(token.str) > 31: reportError(token, 5, 1) def misra_7_1(rawTokens): for tok in rawTokens: if re.match(r'^0[0-7]+$', tok.str): reportError(tok, 7, 1) def misra_7_3(rawTokens): for tok in rawTokens: if re.match(r'^[0-9]+l', tok.str): reportError(tok, 7, 3) def misra_12_1_sizeof(rawTokens): state = 0 for tok in rawTokens: if tok.str.startswith('//') or tok.str.startswith('/*'): continue if tok.str == 'sizeof': state = 1 elif state == 1: if re.match(r'^[a-zA-Z_]',tok.str): state = 2 else: state = 0 elif state == 2: if tok.str in ['+','-','*','/','%']: reportError(tok, 12, 1) else: state = 0 def misra_12_1(data): for token in data.tokenlist: p = getPrecedence(token) if p < 2 or p > 12: continue p1 = getPrecedence(token.astOperand1) if p1 <= 12 and p1 > p and noParentheses(token.astOperand1,token): reportError(token, 12, 1) continue p2 = getPrecedence(token.astOperand2) if p2 <= 12 and p2 > p and noParentheses(token, token.astOperand2): reportError(token, 12, 1) continue def misra_12_2(data): for token in data.tokenlist: if not (token.str in ['<<','>>']): continue if (not token.astOperand2) or (not token.astOperand2.values): continue maxval = 0 for val in token.astOperand2.values: if val.intvalue > maxval: maxval = val.intvalue if maxval == 0: continue sz = bitsOfEssentialType(token.astOperand1) if sz <= 0: continue if maxval >= sz: reportError(token, 12, 2) def misra_12_3(data): for token in data.tokenlist: if token.str != ',': continue if token.astParent and (token.astParent.str in ['(', ',', '{']): continue reportError(token, 12, 3) def misra_12_4(data): max_uint = 0 if INT_BITS == 16: max_uint = 0xffff elif INT_BITS == 32: max_uint = 0xffffffff else: return for token in data.tokenlist: if (not isConstantExpression(token)) or (not isUnsignedInt(token)): continue if not token.values: continue for value in token.values: if value.intvalue < 0 or value.intvalue > max_uint: reportError(token, 12, 4) break def misra_13_1(data): for token in data.tokenlist: if token.str != '=': continue init = token.next if init and init.str == '{' and hasSideEffectsRecursive(init): reportError(init,13,1) def misra_13_3(data): for token in data.tokenlist: if not token.astOperand1: continue if token.astParent and token.astParent.str != ',': continue if token.str == ',': continue if countSideEffects(token) >= 2: reportError(token, 13, 3) def misra_13_4(data): for token in data.tokenlist: if token.str != '=': continue if not token.astParent: continue if not (token.astParent.str in [',', ';']): reportError(token, 13, 4) def misra_13_5(data): for token in data.tokenlist: if token.isLogicalOp and hasSideEffectsRecursive(token.astOperand2): reportError(token, 13, 5) def misra_13_6(data): for token in data.tokenlist: if token.str == 'sizeof' and hasSideEffectsRecursive(token.next): reportError(token, 13, 6) def misra_14_1(data): for token in data.tokenlist: if token.str != 'for': continue exprs = getForLoopExpressions(token) if exprs and hasFloatComparison(exprs[1]): reportError(token, 14, 1) def misra_14_2(data): for token in data.tokenlist: expressions = getForLoopExpressions(token) if not expressions: continue if expressions[0] and not expressions[0].isAssignmentOp: reportError(token, 14, 2) elif hasSideEffectsRecursive(expressions[1]): reportError(token, 14, 2) def misra_14_4(data): for token in data.tokenlist: if token.str != '(': continue if not token.astOperand1 or not (token.astOperand1.str in ['if', 'while']): continue if not isBoolExpression(token.astOperand2): reportError(token, 14, 4) def misra_15_1(data): for token in data.tokenlist: if token.str == "goto": reportError(token, 15, 1) def misra_15_2(data): for token in data.tokenlist: if token.str != 'goto': continue if (not token.next) or (not token.next.isName): continue if not findGotoLabel(token): reportError(token, 15, 2) def misra_15_3(data): for token in data.tokenlist: if token.str != 'goto': continue if (not token.next) or (not token.next.isName): continue tok = findGotoLabel(token) if not tok: continue scope = token.scope while scope and scope != tok.scope: scope = scope.nestedIn if not scope: reportError(token, 15, 3) def misra_15_5(data): for token in data.tokenlist: if token.str == 'return' and token.scope.type != 'Function': reportError(token, 15, 5) def misra_15_6(rawTokens): state = 0 indent = 0 for token in rawTokens: if token.str in ['if', 'for', 'while']: state = 1 indent = 0 elif state == 1: if token.str == '(': indent = indent + 1 elif token.str == ')': if indent == 0: state = 0 elif indent == 1: state = 2 indent = indent - 1 elif state == 2: if token.str.startswith('//') or token.str.startswith('/*'): continue state = 0 if token.str != '{': reportError(token, 15, 6) def misra_15_7(data): for token in data.tokenlist: if token.str != 'if': continue lpar = token.next if not lpar or lpar.str != '(': continue rpar = lpar.link if not rpar or rpar.str != ')': continue brace1 = rpar.next if not brace1 or brace1.str != '{': continue brace2 = brace1.link if not brace2 or brace2.str != '}': continue else_ = brace2.next if not else_ or else_.str != 'else': reportError(token, 15, 7) # TODO add 16.1 rule def misra_16_2(data): for token in data.tokenlist: if token.str == 'case' and token.scope.type != 'Switch': reportError(token, 16, 2) # The goal is handle comments/annotations by other tools def misra_16_3(rawTokens): # state: 0=no, 1=break is seen but not its ';', 2=after 'break;', 'comment', '{' state = 0 for token in rawTokens: if token.str == 'break': state = 1 elif token.str == ';': if state == 1: state = 2 else: state = 0 elif token.str.startswith('/*') or token.str.startswith('//'): if token.str.lower().find('fallthrough')>0: state = 2 elif token.str == '{': state = 2 elif token.str == 'case' and state != 2: reportError(token, 16, 3) if '-verify' in sys.argv[1:]: VERIFY = True for arg in sys.argv[1:]: if not arg.endswith('.dump'): continue data = cppcheckdata.parsedump(arg) if VERIFY: VERIFY_ACTUAL = [] VERIFY_EXPECTED = [] for tok in data.rawTokens: if tok.str.startswith('//') and tok.str.find('TODO')<0: for word in tok.str[2:].split(' '): if re.match(r'[0-9]+\.[0-9]+', word): VERIFY_EXPECTED.append(str(tok.linenr) + ':' + word) else: print('Checking ' + arg + '...') cfgNumber = 0 for cfg in data.configurations: cfgNumber = cfgNumber + 1 if len(data.configurations) > 1: print('Checking ' + arg + ', config "' + cfg.name + '"...') misra_5_1(cfg) if cfgNumber == 1: misra_7_1(data.rawTokens) misra_7_3(data.rawTokens) misra_12_1_sizeof(data.rawTokens) misra_12_1(cfg) misra_12_2(cfg) misra_12_3(cfg) misra_12_4(cfg) misra_13_1(cfg) misra_13_3(cfg) misra_13_4(cfg) misra_13_5(cfg) misra_13_6(cfg) misra_14_1(cfg) misra_14_2(cfg) misra_14_4(cfg) misra_15_1(cfg) misra_15_2(cfg) misra_15_3(cfg) misra_15_5(cfg) if cfgNumber == 1: misra_15_6(data.rawTokens) misra_15_7(cfg) misra_16_2(cfg) if cfgNumber == 1: misra_16_3(data.rawTokens) if VERIFY: for expected in VERIFY_EXPECTED: if not expected in VERIFY_ACTUAL: print('Expected but not seen: ' + expected) sys.exit(1) for actual in VERIFY_ACTUAL: if not actual in VERIFY_EXPECTED: print('Not expected: ' + actual) sys.exit(1)