CppCheckのアドオンをいじってみる（C言語の静的解析)

# 前文
この記事は自身のために残すものです。そのため技術的な解説は無く、成果物のみを掲載しています。
もし私以外に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"の呼び出しを追加

# ソースコード

<details><summary><b>"MyStaticCheck.py"ソースコードを表示（折りたたみ）</b></summary><div>

```python
# 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)
-->
```
</div></details>

## "cert.py"への変更箇所(私が書いたコードではないためコードの掲載無し)
メイン処理（スコープ：if __name__ == '__main__':）の個別のチェックをコールしているスコープ(for cfg in data.iterconfigurations():)に下記の2行を追加

```python
            import MyStaticCheck
            MyStaticCheck.MyStaticCheck(cfg)
```

インポート分からわかるように、"MyStaticCheck.py"ファイルはaddonフォルダに配置することになります。