前文
この記事は自身のために残すものです。そのため技術的な解説は無く、成果物のみを掲載しています。
もし私以外にCppcheckの出力するdumpファイルを解析したい人がいれば、参考となれば幸いです。(非常にニッチ)
なお私はC言語使いのため、コードがPython向けに最適化されておりません。
概要
静的解析を使用してコードの品質向上を行いたいが、有償ソフトの導入はハードルが高いためCppcheckを使用する。
しかし、下記項目のチェックをできないことが不満であったため、アドオンをいじることでチェックを実装する。
・ネイティブの型(UCHAR/SCHAR/USHORT/SSHORT/ULONG/SLONG/FLOAT/DOUBLE)間でのキャストなしの代入を検出する。(広い表現範囲を狭い表現範囲に代入する)
・関数の引数に代入する際、同上のチェックを実施する。
主にUCHAR←SCHARなどのサイズ一致のケースで表現範囲が異なるケースを検出するための機能。
コンパイラによっては警告を出さない場合があるため、検出機能を実装した。
実現方法
下記の順序で実装した。
① Cppcheckが出力するdumpファイルの解析
② addonの"cert.py"を参考に自前の"MyStaticCheck.py"ファイルへ処理の実装
③ addonの"cert.py"から"MyStaticCheck.py"の呼び出しを追加
ソースコード
"MyStaticCheck.py"ソースコードを表示(折りたたみ)
# cppcheckのdumpファイルを対象としたオリジナルの静的解析プログラム
# cppcheckのAddOnから呼び出す。
# 現時点ではcert.pyから呼び出すことを想定
import argparse
import cppcheckdata
import sys
import re
REMARKE = "AC"
global g_NameTokenList # tokenListをisNameプロパティでFilterしたリストを格納する
global g_FuncTokenList # tokenListを関数宣言・関数呼び出しでFilterしたリストを格納する ←これできる?
global g_GlovalTokenList # tokenListをglovalscopeでFilterしたリストを格納する
class EmptyClass:
pass
def isNumber(strNum):
"""
入力された文字列が数値を示すならばTRUE
そうでなければFALSEを返す
"""
try:
#10進数実数
float(strNum)
return True
except:
try:
# 接頭辞で書式判断
int(strNum,0)
return True
except:
return False
return False
def getTokenById(tokenList, Id):
"""
TokenListをIdで走査しHitしたTokenを返す。
Hitしない場合はNoneを返す
"""
for token in tokenList:
if(token.Id == Id):
return token
return None
#"("がキャスト書式の先頭を示しているかを確認する関数
#引数 expr:dumpのtokenlistのtoken
def isCast(expr):
if not expr or expr.str != '(' or not expr.astOperand1 or expr.astOperand2:
return False
if cppcheckdata.simpleMatch(expr, '( )'):
return False
return True
def serchValDef(tokenList, token):
"""
isName = Trueのtokenをもとに、その変数名の定義を検索し
型情報を持つTokenを返す。
Scope情報をもとにtokenをさかのぼり、検索する
tokenListではなくvariablesから探したら楽かも?→法則性がわからないのでNG
gloval, extern glovalの変数は取れる?→テストにより取れることを確認済
TODO:現在は1変数ごとに毎回ソースを走査するため非常に遅い。最初に辞書を作る等そのうち高速化を図ること
→エントリ関数(main相当)でFilter関数を用いてisName=trueのデータをリスト化したものを使用する
引数tokenが上記のFilter済リストに含まれているため、その位置を検索し、その位置からFilter済リストを上方検索する
Hitした場合、Filter済リストの要素がもつ前後関係のToken情報を使用してデータを追跡する
"""
tkn = token.previous
while tkn != None:
if tkn.isName:
if tkn.str == token.str:
if tkn.valueType:
# hitした変数が型情報を持っている場合
return tkn
else:
if tkn.astParent:
if isCast(tkn.astParent):
# hitした変数がキャストしている場合
# 注意:(キャストしているからと言って型情報を持っているわけではない。)
return tkn.astParent
elif tkn.astParent.str == ",":
# カンマを使用し、複数の変数を宣言している場合、astOperand1でさかのぼって型を確認する
commaToken = tkn.astParent
while commaToken.astOperand1 != None:
if commaToken.astOperand1.valueType:
return commaToken.astOperand1
commaToken = commaToken.astOperand1
# tknを更新する
tkn = tkn.previous
return None
def getFuncRetType(tokenList, token):
'''
tokenListをgroval scopeでfilterした結果で関数名を検索する
hitした戻り値の型情報をもつtokenを返す。
見つからない場合はNoneを返す
'''
if not token.isName:
return None
global g_FuncTokenList # 関数宣言のList
global g_GlovalTokenList # GlovalScopeのTokenList
for fncTkn in g_FuncTokenList:
if fncTkn.isName:
if fncTkn.str == token.str:
# grobal scopeで関数名がhitした場合
if fncTkn.valueType:
return fncTkn
else:
# 関数名が型を持っていない場合、関数のastParentと同じOperand1をもつToken(関数のキャストTokenが期待値)を
# 遡って探し、それを返す
prvToken = fncTkn.previous
for i in range(5):
if prvToken.astOperand1Id == fncTkn.astParentId:
if isCast(prvToken):
return prvToken
prvToken = prvToken.previous
if prvToken == None:
break
# hitなし
return None
def getStructMemberType(tokenList, token):
'''
tokenと結びつく構造体メンバの型情報を返す。
hitしない場合はNoneを返す
tokenから構造体local宣言へアクセスし、構造体のScope情報を得る
Scope情報のbodyStart~bodyEndの範囲を
メンバ名で走査し、hitした型情報を返す
tokenListは使用していないがIF合わせで引数としてとる
'''
structScope = token.astOperand1.valueType.typeScope
memberToken = token.astOperand2
strmemb = structScope.bodyStart
while(strmemb.Id != structScope.bodyEnd):
if(strmemb.str == memberToken.str):
if strmemb.valueType:
return strmemb
else:
if strmemb.astParent:
if isCast(strmemb.astParent):
# hitした変数がキャストしている場合
# 注意:(キャストしているからと言って型情報を持っているわけではない。)
return strmemb.astParent
strmemb = strmemb.next
return None
def getValueTyoeTokenRecursive(tokenList, token):
"""
getValueTypeTokenから呼び出し再帰的に使用する
入力されたtokenの型情報をtokenListから検索し
型定義しているtokenを返す。
見つからない場合や入力Tokenが不適切な場合Noneを返す
"""
if not token.valueType:
if token.str =="." or token.str == "->":
# 構造体メンバ参照の場合、構造体メンバを示すTokenで再帰した場合、変数名と同じ扱いになるためNG
#if token.astOperand2 != None:
# nextToken = token.astOperand2
#else:
# nextToken = False
nextToken = getStructMemberType(tokenList, token)
elif token.str == "(":
# 関数の場合や(を伴う演算の代入の場合,関数名を示すTokenで再帰した場合、変数名と同じ扱いになるためNG
if token.astOperand1 != None:
nextToken = getFuncRetType(tokenList, token.astOperand1)
else:
nextToken = None
elif token.str == "{":
# 配列の初期化の場合
nextToken = token.next
elif token.isName:
# 変数名の場合
nextToken = serchValDef(tokenList, token)
else:
nextToken = None
if nextToken:
return getValueTyoeTokenRecursive(tokenList, nextToken)
else:
return None
else:
# サイズ情報を持つ場合
return token
def getValueTypeToken(tokenList, token):
"""
入力されたtokenの型情報をtokenListから検索し
型定義しているtokenを返す。
"""
return getValueTyoeTokenRecursive(tokenList, token)
convType2InxDict = {"char":0,"short":2,"long":4,"int":6,"float":8,"double":9}
assignChkTabl =[
[1,0,0,0,0,0,0,0,0,0], # char+
[0,1,0,0,0,0,0,0,0,0], # char-
[1,0,1,0,0,0,0,0,0,0], # short+
[1,1,0,1,0,0,0,0,0,0], # short-
[1,0,1,0,1,0,0,0,0,0], # long+
[1,1,1,1,0,1,0,0,0,0], # long-
[1,0,1,0,1,0,1,0,0,0], # int+
[1,1,1,1,0,1,0,1,0,0], # int-
[1,1,1,1,0,0,0,0,1,0], # float
[1,1,1,1,1,1,0,0,1,1] # double
]
def isErrorAssign(toToken, frmToken):
"""
型情報を持つTokenを比較して、代入処理のレンジがアンマッチではないか確認する
直値の範囲外代入はコンパイラで検出できるものとし、扱わない
Parameters
----------
toToken : 代入先
frmToken: 代入元
"""
if toToken == None or frmToken == None:
return None
if toToken.valueType == None or frmToken.valueType == None:
return None
if isNumber(frmToken.str):
string = "<OK>set imidiate Value {}".format(frmToken.str)
return string
# 型と符号の比較を行う。比較結果をテーブルに定義し、型と符号の組み合わせでそれを牽く
toValType = toToken.valueType
fmValType = frmToken.valueType
toIdx = convType2InxDict.get(toValType.type, None)
if toIdx != None and toIdx != 8 and toIdx !=9:
if toValType.sign == "signed":
toIdx = toIdx+1
fmIdx = convType2InxDict.get(fmValType.type, None)
if fmIdx != None and fmIdx != 8 and fmIdx !=9:
if fmValType.sign == "signed":
fmIdx = fmIdx+1
if toIdx == None or fmIdx == None:
return None
isOK = assignChkTabl[toIdx][fmIdx]
mes = "OK" if(isOK == 1) else "Type Check NG"
string = "<{}>[{}][{}]Left:{},{} Right:{},{}".format(mes,toIdx,fmIdx,toValType.sign, toValType.type, fmValType.sign, fmValType.type)
return string
def init(tokenList):
"""
下記のグローバル変数(List型)の作成を行う
g_NameTokenList # tokenListをisNameプロパティでFilterしたリストを格納する
g_FuncTokenList # g_NameTokenListをnextToken="("かつGlovalscopeでFilterしたリストを格納する
g_GlovalTokenList # tokenListをglovalscopeでFilterしたリストを格納する
"""
global g_NameTokenList
global g_FuncTokenList
global g_GlovalTokenList
g_NameTokenList = list(filter((lambda x:(x.isName)), tokenList ))
gvlScope = tokenList[0].scopeId
g_FuncTokenList = list(filter((lambda x:(x.next and x.next.str == "(" and x.scopeId == gvlScope)), g_NameTokenList ))
g_GlovalTokenList = list(filter((lambda x:(x.scopeId == gvlScope)), tokenList ))
return
def AssignRangeCheck(tokenList, token):
"""
明示的キャストをしない、表現範囲がラップしない代入処理を検出する。
例 UCHAR = SCHAR, UCHAR = USHORT, ULONG = SCHARなど
対象はchar short longサイズ
Parameters
----------
tokenList : List
dumpファイルのtokenlist
token: token
token.strが関数名を示すtoken
"""
if(token.str == '='):
if not token.astOperand1:
return
if not token.astOperand2:
return
if not token.isAssignmentOp: # 代入でない場合はスルー
return
if token.astOperand2.isOp: # 四則演算などの処理の場合スルー
return
# 代入先、代入元tokenを取得する
toToken = token.astOperand1
frmToken = token.astOperand2
if isCast(frmToken):
# 代入元がキャストしているならばチェック外
mes = "<OK>Right is casted"
else:
# 代入先、代入元の型を取得する
toValueTypeToken = getValueTypeToken(tokenList, toToken)
frmValueTypeToken = getValueTypeToken(tokenList, frmToken)
mes = isErrorAssign(toValueTypeToken, frmValueTypeToken)
if mes:
cppcheckdata.reportError(token, "warning", mes, REMARKE, "AC-01")
def isStrEqType(token):
"""
使用禁止関数(意図通りに動かない)
符号情報があり、
strパラメータがchar, short, long, int, float, doubleの場合
valueTypeに型情報をセットしてTrueを返す関数。
符号情報があてにならないため意図通りに動作できなかった。
"""
if(token.isUnsigned == None):
return False
if(token.isUnsigned == token.isSigned):
return False
if(token.isSigned):
sign = "signed"
else:
sign = "unsigned"
types = ["char","short","long","int","float","double"]
for typeStr in types:
if token.str == typeStr:
myValtyp = EmptyClass()
myValtyp.sign = sign
myValtyp.type = token.str
token.valueType = myValtyp
return True
return False
def getTageArgList(token):
"""
関数名を示すtokenをもとにg_FuncTokenListを走査し、引数のリストを作成しreturnする
・引数の型がnativeの型(char int doubleなど)の場合 該当の型情報をもつtoken
・上記以外の場合 None
注:引数に変数名が無い場合、検出できない
Parameters
----------
token: token
token.strが関数名を示すtoken
return
native型の引数List, 変数名なし引数をもつ関数のToken(引数全てに名前がある場合None)
hitしない場合はNone, None
引数が無い場合はNone, None
"""
STATE_SERCH_TYPE = 0 # 検索状態
STATE_SERCH_COMMA = 1 # 検索状態
isArgNoName = None
for fnc in g_FuncTokenList:
if fnc.str == token.str:
fncDef = fnc.next.next # 関数名.nextの"("は関数の戻り値の型のため更に進める
argList = []
state = STATE_SERCH_TYPE
# 関数名~(; or {)までループし、引数の情報をargListへ抽出する
while (not fncDef.str == ";" and not fncDef.str == "{"):
if state == STATE_SERCH_TYPE:
if fncDef.valueType or isStrEqType(fncDef):#宣言から型が取れないケースを救おうとしたが失敗した
argList.append(fncDef)
state = STATE_SERCH_COMMA
elif fncDef.str == "," or fncDef.str == ")":
# 型情報取得前の場合は型が取れなかったためNoneを追加する
argList.append(None)
isArgNoName = fncDef
else:
if fncDef.str == ",":
state = STATE_SERCH_TYPE
fncDef = fncDef.next
if len(argList) == 0:
# 引数なしの場合はNoneを返す
return None, None
return argList , isArgNoName
return None, None
def getCompArgList(tokenList, token):
"""
関数名を示すtokenをもとに呼び出しで設定した引数の型リストを作成しreturnする
・引数の型がnativeの型(char int doubleなど)の場合 該当の型情報をもつtoken
・上記以外の場合 None
Parameters
----------
token: token
token.strが関数名を示すtoken
return
native型の引数List, 変数名なし引数をもつ関数のToken(引数全てに名前がある場合None)
hitしない場合はNone, None
引数が無い場合はNone, None
"""
STATE_SERCH_TYPE = 0 # 検索状態
STATE_SERCH_COMMA = 1 # 検索状態
# 関数名~(; or {)までループし、引数の情報をargListへ抽出する
funcToken = token.next
argList = []
state = STATE_SERCH_TYPE
while (not funcToken.str == ";" and not funcToken.str == "{"):
if state == STATE_SERCH_TYPE:
if funcToken.isName:
ret = serchValDef(tokenList, funcToken)
argList.append(ret)
state = STATE_SERCH_COMMA
elif funcToken.str == "," or funcToken.str == ")":
# 型情報取得前の場合は型が取れなかったためNoneを追加する
argList.append(None)
else:
if funcToken.str == ",":
state = STATE_SERCH_TYPE
funcToken = funcToken.next
if len(argList) == 0:
# 引数なしの場合はNoneを返す
return None
return argList
def FuncArgumentCheck(tokenList, token):
"""
明示的キャストをしない、表現範囲がラップしない引数入力を検出する。
例 UCHAR = SCHAR, UCHAR = USHORT, ULONG = SCHARなど
対象はchar short longサイズ
Parameters
----------
data : iterconfiguration
dumpファイルを読み込んだ結果
"""
gvlScope = tokenList[0].scopeId
if token.scopeId == gvlScope:
return
if not token.isName:
return
if not token.next:
return
if not token.next.str == "(":
return
## 関数呼び出し箇所に対して解析を行う##
# 関数定義から引数の数と解析対象の引数を求める。(解析対象:charやintなどnativeの型の引数)
tageArgList, nameLessArgFunction = getTageArgList(token)
# 上記関数で引数の型が取れないケースがあるが、その場合の異常はスルーする。(関数宣言時の引数にキャストがあると取れない?)
#if nameLessArgFunction:
# cppcheckdata.reportError(nameLessArgFunction, "style", "関数宣言の引数には変数名を記載ください", REMARKE, "AC-02")
if not tageArgList:
return
# hitした関数呼び出し箇所から引数の型リストを作成する
compArgList = getCompArgList(tokenList, token)
if not compArgList:
return
if not len(tageArgList) == len(compArgList):
return #要素数が一致しない場合はスルー コンパイラでエラーとなるため警告しない
for i in range(len(tageArgList)):
defArg = tageArgList[i]
setArg = compArgList[i]
mes = isErrorAssign(defArg, setArg)
if mes:
cppcheckdata.reportError(token, "warning", mes, REMARKE, "AC-02")
return
def ArrayIndexCheck(tokenList, token):
"""
配列のindexを入力する際にオーバーするかどうかをチェックする
Parameters
----------
data : iterconfiguration
dumpファイルを読み込んだ結果
"""
if(token.str == "["):
#配列定義がhitしたか参照がhitしたかを判断する
token.isName
def MyStaticCheck(data):
"""
オリジナルチェックを実施する。実施項目は下記の通り
AssignRangeCheck:代入型チェック(ポインタは対象外,演算結果の代入は対象外)
FuncArgumentCheck:関数の引数型チェック(ポインタは対象外,演算結果の引数は対象外)
Parameters
----------
data : iterconfiguration
dumpファイルを読み込んだ結果
"""
init(data.tokenlist)
for token in data.tokenlist:
AssignRangeCheck(data.tokenlist, token)
FuncArgumentCheck(data.tokenlist, token)
#未実装ArrayIndexCheck(data.tokenlist, token)
-->
"cert.py"への変更箇所(私が書いたコードではないためコードの掲載無し)
メイン処理(スコープ:if name == 'main':)の個別のチェックをコールしているスコープ(for cfg in data.iterconfigurations():)に下記の2行を追加
import MyStaticCheck
MyStaticCheck.MyStaticCheck(cfg)
インポート分からわかるように、"MyStaticCheck.py"ファイルはaddonフォルダに配置することになります。