記事
· 2024年10月14日 15m read

SourceControlを用いた自動ソースチェックツールについて

開発者の皆様はじめまして。
私からはIRISのソースコントロール機能を用いたソースの自動チェック機能のご紹介をしたいと思います。
チーム開発では、ソースの可読性や実装方法等がある程度統一されるようにコーディング規約を作成すると思います。
しかし、メンバーの入れ替わりでコーディング規約の説明をしていても徹底されないことが起こることも少なくありません。
なので、ソースコントロールを使用してコンパイル時に自動的にチェックするようにしました。
IRIS内で完結させるメリットとして、エラーチェックだけでなくチェック後にエラーがなければコンパイルまで自動で行えること、
%Dictionary.ClassDefinition(クラス定義)を使用できるので、チェッククラスを作成しやすいこと等があげられます。

目次

  1. ソースコントロールについて
  2. 今回用意したチェック用クラスの紹介
  3. ソースコントロールへの設定
  4. 実際の動作
  5. 感想

1.ソースコントロールについて
まず、ソースコントロールについて簡単に記載します。
ソースコントロールとは、一般的にコードに対する変更を追跡し管理することを表します。
IRISのソースコントロール機能には様々なメソッドが用意されています。
今回はそれを使用することでソースの自動チェック機能を実現していきます。
参考リンク:InterSystems IRIS とソース・コントロール・システムの統合、 ソース・コントロール設定の構成

2.今回用意したチェック用クラスの紹介
今回作成したチェック用クラスには基底クラスとして、「%Studio.SourceControl.Base」と「%Studio.Extension.Base」(%Studio.SourceControl.Baseの基底クラス)を使用しています。
上記のクラスにはログイン時のイベントやロード前イベントなどが定義されており、今回は「OnBeforeCompile」(コンパイル前イベント)を使用しました。
image

では、実際のチェック用クラスの内容をご紹介します。
今回は以下をコーディング規則として実装をしています。
①クラスの命名チェック - 「XXXX」始まりのクラス名であること
②インデントチェック - インデントは4の倍数の半角空白で埋めること
③変数の利用チェック - 定義した変数は利用すること
④引数の利用チェック - パラメータとして受け取った引数は利用すること

Class User.CompileChk Extends %Studio.SourceControl.Base
{

///  処理概要 :コンパイル前チェック
///  <br>IN :InternalName : コンパイル対象クラス
///  <br>OUT :%Status
///  <br>処理詳細:規約に則さない実装がされている場合、コンパイルエラーにする。
Method OnBeforeCompile(InternalName As %String, ByRef qstruct As %String) As %Status
{
    Set InternalName = $REPLACE(InternalName, ".CLS", "")
    Set clsDef = ##Class(%Dictionary.ClassDefinition).%OpenId(InternalName)

    Set SKIPuFLG = $$$NO
    Write !,"****************コンパイルチェック開始****************",!
    Write "TARGET : "_InternalName,!
    Set hasErr = 0
    
    #; クラス名チェック
    Set hasErr = hasErr + '##class(User.Chk.ClassNamingChecker).%New().IsCorrectDefine(clsDef)
    #; インデントチェック
    Set hasErr = hasErr + '##class(User.Chk.IndentChecker).%New().UseWrongIndent(clsDef)
    #; 変数の利用チェック
    Set hasErr = hasErr + '##class(User.Chk.UseValChecker).%New().UseVal(clsDef)
    #; 引数の利用チェック
    Set hasErr = hasErr + '##class(User.Chk.UseArgsChecker).%New().UseArgs(clsDef)
    

    Write "****************コンパイルチェック完了****************",!
    If (hasErr > 0) {
        Return $$$ERROR($$$GeneralError, "コンパイルエラーがあります。")
    } Else {
        Return $$$OK
    }
}

}

①クラスの命名チェッククラス - User.Chk.ClassNamingChecker
Class User.Chk.ClassNamingChecker Extends %RegisteredObject
{

///  処理概要 :クラス定義の命名規約違反チェック
///  <br>IN :clsDef : クラス定義
///  <br>OUT :%Boolean
///  <br>処理詳細:クラスの命名がコーディング規約に従っているかどうかをチェックする。
Method IsCorrectDefine(clsDef As %Dictionary.ClassDefinition) As %Boolean
{
    Write "*クラス名チェック",!

    Set ret = $$$YES
    If (clsDef '= "") {
        Set clsName = clsDef.Name
        Set ret = ..WriteStartWithStrErr(clsName)
    }

    Return ret
}

///  処理概要 :命名先頭不正のエラー表示
///  <br>IN :clsName クラス名/ keyword キーワード
///  <br>OUT :%Boolean
///  <br>処理詳細:クラス名の先頭がキーワードで開始していなければエラーを表示する。
Method WriteStartWithStrErr(clsName As %String) As %Boolean [ Private ]
{
    If ($FIND(clsName, ".XXXX") > 0) {
        #; OK
        Return $$$YES
    } Else {
        Write "E: クラス名はXXXXという単語で始まる必要があります。",!
        Return $$$NO
    }
}

}

クラス内で行っていること
引数として%Dictionary.ClassDefinition(クラス定義)を受け取り、クラス定義内のプロパティであるNameを使用することで
クラス名を取得。取得したクラス名に対して、「.XXXX」を$FINDで検索することでクラスの先頭が「XXXX」であるかチェックを行います。

②インデントチェック - User.Chk.IndentChecker

Class User.Chk.IndentChecker Extends %RegisteredObject
{

///  処理概要 :インデント不正チェック
///  <br>IN :clsDef : クラス定義
///  <br>OUT :%Boolean
///  <br>処理詳細:インデントが4の倍数になっているかをチェックをする。
Method UseWrongIndent(clsDef As %Dictionary.ClassDefinition) As %Boolean
{
    Write "*インデント不正チェック",!
    Set isCorrect = $$$YES
    Set count = clsDef.Methods.Count()
    For i = 1: 1: count {
        Set cnt = 0
        Set method = clsDef.Methods.GetAt(i)
        Do method.Implementation.Rewind()

        While ('method.Implementation.AtEnd) {
            Set cnt = cnt + 1
            Set line = method.Implementation.ReadLine()
            If (line = "") {
                Continue
            }

            If ('$MATCH(line, "^( {4}){1,}[^ ].*")) {
                Set isCorrect = $$$NO
                Write "E: インデントが4の倍数になっていません。: "_method.Name_"+"_cnt_line,!
            }
        }
    }

    Return isCorrect
}

}

クラス内で行っていること
引数として%Dictionary.ClassDefinition(クラス定義)を受け取り、クラス定義内のプロパティであるMethods.Count()でメソッドの数を取得。
メソッドごとに1行ずつチェックを行います。チェックは正規表現を使用(^( {4}){1,}[^ ].*の部分)し、半角スペースが4の倍数になっているかチェックを行います。

③変数の利用チェック - User.Chk.UseValChecker

Class User.Chk.UseValChecker Extends %RegisteredObject
{

///  処理概要 :変数の利用チェック
///  <br>IN :clsDef : クラス定義
///  <br>OUT :%Boolean
///  <br>処理詳細:定義された変数が利用されているかをチェックをする。
Method UseVal(clsDef As %Dictionary.ClassDefinition) As %Boolean
{

    Set isCorrect = $$$YES
    Set count = clsDef.Methods.Count()
    For i = 1: 1: count {
        Set cnt = 0
        Set method = clsDef.Methods.GetAt(i)
        Do method.Implementation.Rewind()

        Set args = method.FormalSpec
        Set argList = {}
        For j = 1: 1: $LENGTH(args, ",") {
            Set arg = $REPLACE($REPLACE($REPLACE($PIECE($PIECE(args, ",", j), ":", 1), "&", ""), "*", ""), "...", "")
            If (arg = "") {
                Continue
            }
            Do argList.%Set(arg, "")
        }

        Set valList = {}
        While ('method.Implementation.AtEnd) {
            Set cnt = cnt + 1
            Set line = method.Implementation.ReadLine()
            If (line = "") {
                Continue
            }
            If (..HasComment(line)) {
                Continue
            }

            If ($FIND(line, "Set ") > 0) {
                Set valNm = $PIECE($REPLACE($PIECE($PIECE(line, "Set ", 2), "="), " ", ""), "(")
                #; オブジェクトへの参照は対象外。
                If (($FIND(valNm, ".") = 0) && ($FIND(valNm, "$") = 0)) {
                    #; 変数の定義があればObjectに登録。
                    If (('valList.%IsDefined(valNm)) && 'argList.%IsDefined(valNm)) {
                        Do valList.%Set(valNm, $$$NO)
                    }
                }
            }

            Set iter = valList.%GetIterator()
            While iter.%GetNext(.key, .value) {
                If ($FIND($REPLACE(line, "Set "_key_" ", ""), key) > 0) {
                    Do valList.%Set(key, $$$YES)
                }
            }
        }
        Set iter = valList.%GetIterator()
        While iter.%GetNext(.key, .value) {
            If ('value) {
                Write "E: "_method.Name_"() にて変数"_key_"は定義されていますが、利用されていない可能性があります。",!
                Set isCorrect = $$$NO
            }
        }
    }
    Return isCorrect
}

Method HasComment(line As %String) As %Boolean [ Private ]
{
    Return ($MATCH(line, "^( )*#;.*") > 0) || ($MATCH(line, "^( )*//.*") > 0)
}

}

クラス内で行っていること
②で行ったようにメソッド単位でチェックを行います。method.FormalSpec でメソッドの引数のリストを含む文字列を取得します。
上記で取得した文字列から、引数のみを抽出して引数リストを作ります。ここまで来たら、メソッドを1行ずつチェックしていきます。
HasCommentメソッドでコメントの場合は読み飛ばすようにしています。まず、「Set」の使用をチェックします。(If ($FIND(line, "Set ") > 0) {)
使用されている場合でもオブジェクトへの参照は対象外とするため、「.」や「$」が使用されている場合は読み飛ばします。(If (($FIND(valNm, ".") = 0) && ($FIND(valNm, "$") = 0)) {)
「.」や「$」が使用されていないかつ、引数のリストに存在しない場合は後のチェックのためにリストに追加します。(value側は$$$NOで登録しておきます)
チェックリストを順番にリストのキー項目が定義箇所以外で使用されているかチェックしていきます。(L50~L54)
使用されている場合、該当キー項目のvalueを$$$YESに更新しておきます。
チェック処理としては上記で完了です。あとはvalueが$$$NOの項目を洗い出して、チェック完了となります。

④引数の利用チェック - User.Chk.UseArgsChecker

Class User.Chk.UseArgsChecker Extends %RegisteredObject
{

///  処理概要 :引数の利用チェック
///  <br>IN :clsDef : クラス定義
///  <br>OUT :%Boolean
///  <br>処理詳細:引数が利用されているかをチェックする。
Method UseArgs(clsDef As %Dictionary.ClassDefinition) As %Boolean
{
    Write "*引数の利用チェック",!
    Set ngKeyword = $LISTBUILD(")", """", "}")

    Set isCorrect = $$$YES
    Set count = clsDef.Methods.Count()
    For i = 1: 1: count {
        Set method = clsDef.Methods.GetAt(i)
        Set args = method.FormalSpec

        Do method.Implementation.Rewind()
        Set str = method.Implementation.Read()
        For j = 1: 1: $LENGTH(args, ",") {
            Set arg = $REPLACE($REPLACE($REPLACE($PIECE($PIECE(args, ",", j), ":", 1), "&", ""), "*", ""), "...", "")
            If (arg = "") {
                Continue
            }

            Set isIgnore = $$$NO
            Set ptr = 0
            While $LISTNEXT(ngKeyword, ptr, value) {
                #; NGリストの内容が含まれていると、切り出し対象外。
                If ($FIND(arg, value) > 0) {
                    Set isIgnore = $$$YES
                    Quit
                }
            }
            If (isIgnore) {
                Continue
            }

            If ($FIND(str, arg) = 0) {
                Write "E: 引数が利用されていません。: "_method.Name_"/"_arg,!
                Set isCorrect = $$$NO
            }
        }
    }

    Return isCorrect
}

}

クラス内で行っていること
まず、コメントなどを引っ掛けないためにチェックしない文字を定義します。(L10)
③と同様にメソッド単位でチェックをするようにします。(L13~)
メソッドの引数を取得します。(L16)
メソッドの最初の行からチェックするようにポインタをストリームの先頭にしてメソッドの読込を行います。(L18、L19)
引数の数分チェックをまわしていきます。(L20)
引数の中にチェックしない文字が入っているかチェックします。(L28~L34)
チェックしない文字が入っていない場合は、読み込んだメソッドの中で引数が使用されているかチェックを行います。(L39~L42)

3.ソースコントロールへの設定
管理ポータルにて、[システム管理]⇒[構成]⇒[追加の設定]⇒[ソースコントロール] を開くと
ソースコントロールの設定画面となります。
image
image

ソースコントロールクラス名の一覧には「%Studio.SourceControl.Base」クラスを継承したクラスが表示されます。
ソースコントロールを行いたいネームスペースを選択し、使用したいソースコントロールクラスを選択⇒保存します。
今回はUSERのネームスペースにチェック用クラス用意していますが、%SYSにソースコントロールクラスを作成することで全てのネームスペースに対して使用することができます。

4.実際の動作
今回テスト用に用意したクラスが以下のクラスです。

Class User.Test.NewClass1 Extends %RegisteredObject
{

///  処理概要 :テストメソッド
///  <br>IN :Str1 : テスト用文字列1
///  <br>IN :Str2 : テスト用文字列2
///  <br>OUT :%Boolean
Method TestMethod(Str1 As %String, Str2 As %String) As %Boolean
{
   Set test1 = Str1
    Set test2 = "TEST"
    
    Set ^TESTG(test1,"abc") = "hugehuge"
    
    Return $$$OK
}

}

クラス名が「XXXX」始まりでないこと。 - クラスの命名チェック
インデントが4の倍数個の半角スペースになっていないこと。(L10) - インデントチェック
定義した変数test2が使用されていないこと。 - 変数の使用チェック
TestMethodの引数として用意したStr2が使用されていないこと。 - 引数の使用チェック

実際にコンパイルした結果がこちらです。
image

すべてチェックに引っかかっており、コンパイルもされないようになっています。
では、チェッククラスに指摘された部分を修正したものをコンパイルしてみます。

Class User.Test.XXXXNewClass1 Extends %RegisteredObject
{

///  処理概要 :テストメソッド
///  <br>IN :Str1 : テスト用文字列1
///  <br>IN :Str2 : テスト用文字列2
///  <br>OUT :%Boolean
Method TestMethod(Str1 As %String, Str2 As %String) As %Boolean
{
    Set test1 = Str1
    Set test2 = "TEST"
    Set ^TESTG(test1,test2) = Str2
    Return $$$OK
}

}

image

見事にコンパイルが成功しました。

5.感想
今回参考例として4つのチェックを行いましたが、工夫や組み込み方次第では色々なチェックを組み込めると感じました。
Ex.) 変数がキャメルケースになっているか、利用してほしくないプロパティ等が使用されているか etc…
また、他のライブラリでチェックツールは色々とあるかと思いますが、今回はIRISの中だけで完結させており、
チェックだけでなくエラーが出なかった時はコンパイルまで通るところがやはり良い部分に感じました。
今回使用したクラスはGithubにアップしておりますので、興味のある方はご確認いただければと思います。
追記)インデントチェッククラスをEmbedded Pythonで記載してみました。(Githubにもアップしております)

Class User.Chk.IndentCheckerP Extends %RegisteredObject
{

///  処理概要   :インデント不正チェック
///  <br>IN     :clsDef : クラス定義
///  <br>OUT    :%Boolean
///  <br>処理詳細:インデントが4の倍数になっているかをチェックをする。
ClassMethod UseWrongIndentP(clsDef As %Dictionary.ClassDefinition) As %Boolean [ Language = python ]
{
    import iris
    import re

    print("*インデント不正チェック\n")

    isCorrect = 1
    count = clsDef.Methods.Count()
    for i in range(count):
        cnt = 0
        method = clsDef.Methods.GetAt(i + 1)
        while not method.Implementation.AtEnd:
            cnt += 1
            line = method.Implementation.ReadLine()
            if line == '':
                continue
            if len(re.compile("^( {4}){1,}[^ ].*").findall(line)) == 0:
                isCorrect = 0
                print("E: インデントが4の倍数になっていません。: " + method.Name + str(cnt) + str(line) + "\n") 
    return isCorrect
}

}

以上になります。ご一読いただき、ありがとうございました。

ディスカッション (0)1
続けるにはログインするか新規登録を行ってください