0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-04-16

前文

この記事は自身のために残すものです。そのため技術的な解説は無く、成果物のみを掲載しています。
もし私以外に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フォルダに配置することになります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?