電通総研 テックブログ

電通総研が運営する技術ブログ

IntelliJプラグイン開発の始め方~コード検査編~

こんにちは。電通総研ITの寺尾です。 今回はIntelliJインスペクション(コード検査)機能の実装方法についてご紹介します。

前回はこちら:IntelliJプラグイン開発の始め方~ラインマーカー編~

インスペクション(コード検査)とは

インスペクションとは、IntelliJ上で特定の条件を満たす箇所にエラー、警告を表示するコード検査機能です。
開発者はこのハイライトを目印にしてコード修正をすることが多いかと思います。
IntelliJプラグインでも独自の検査ルールを用意し、エラーハイライトや警告表示を行えます。

公式ドキュメント

実装前に知っておくこと

前回と同様に、本記事でもKotlinでの実装例を提示します。

Kotlinプロジェクトに関する内容は、アクション機能編の実装前に知っておくことをご参照ください。

実装

ゴール

今回は、DAOメソッドに対するSQLテンプレートの存在をチェックする検査機能を実装します。
クイックフィックスの実装については省略させていただきますので、ご了承ください。

ExistsSQLInspection

事前準備

インスペクション機能はプロバイダークラスを実装して実現します。

まずは以下の準備から始めていきましょう。

  • InspectionToolProviderのサブクラス
  • AbstractBaseJavaLocalInspectionToolのサブクラス
  • JavaElementVisitorのサブクラス
  • インスペクション機能の説明HTMLファイル
  • プラグイン設定登録

コード検査機能では、プロバイダークラスを起点に以下の流れでコード検査処理を呼び出します。

Provider⇒Inspection⇒Visitor

plugin.xmlには、以下のように<inspectionToolProvider>タグを使ってプロバイダークラスを登録します。
プロバイダークラスはIntelliJの拡張ポイントとして登録するため、<extensions defaultExtensionNs="com.intellij">タグの中に記述します。

 <inspectionToolProvider implementation="org.domaframework.doma.intellij.inspection.dao.provider.SqlFileExistProvider" />

それでは各実装について一つずつ見ていきます。

プロバイダークラスの実装

設定ファイルに登録するプロバイダークラスの実装から始めていきます。

ここでの実装はとてもシンプルで、以下のようにオーバーライドメソッドでインスペクションクラスのリストを返すのみになります。

class SqlFileExistProvider : InspectionToolProvider {
    override fun getInspectionClasses(): Array<Class<out LocalInspectionTool>> =
        arrayOf(
            SqlFileExistInspection::class.java,
        )
}

インスペクションクラスの実装

プロバイダークラスから渡されるインスペクションクラスを実装します。

このクラスでは、インスペクション名称やグループなどの基本情報の定義と、コード内の要素を探索して処理するVisitorクラスを呼び出します。
ここで定義したインスペクション情報は、IntelliJ設定 > エディター > インスペクションで表示される情報です。
またshortNameはインスペクションの識別名となり、後ほど作成するHTMLファイル名は識別名と一致している必要があります。

class SqlFileExistInspection : AbstractBaseJavaLocalInspectionTool() {
    // インスペクション表示名
    override fun getDisplayName(): String = "Check existence of SQL file"

    // インスペクション名
    override fun getShortName(): String = "org.domaframework.doma.intellij.existsqlchecker"

  // インスペクショングループ名
    override fun getGroupDisplayName(): String = "DomaTools"

    // デフォルト有効フラグ
    override fun isEnabledByDefault(): Boolean = true

  // デフォルトのエラーレベル
    override fun getDefaultLevel(): HighlightDisplayLevel = HighlightDisplayLevel.ERROR

  // Visitorオブジェクトの取得
    override fun buildVisitor(
        holder: ProblemsHolder,
        isOnTheFly: Boolean,
    ): PsiElementVisitor = SqlFileExistInspectionVisitor(holder, this.shortName)
}

Visitorクラスの実装

コード検査ロジック本体をVisitorクラスに実装します。
Visitorクラスは様々な種類がありますが、今回はJavaで実装されたDAOファイルに対する要素の探索を行うため、JavaElementVisitorのサブクラスとして実装します。

このクラスはサポートしている要素ごとにvisitメソッドをオーバーライドし、その要素に対して処理を行います。
JavaElementVisitorのサブクラスでは、例えば以下の要素の探索が可能となっています。

  • クラス定義
  • メソッド定義
  • メソッドアノテーション
  • メソッドパラメータリスト
  • メソッドパラメータ
  • コードブロック

定義全体、またはその子要素など細かいメソッドが用意されています。
今回はアノテーションも含めたメソッド定義の情報を扱うため、visitMethodのみオーバーライドします。

コード検査の条件として、以下のチェックロジックを実装します。

  • 対象がJavaのDAOファイルである
  • SQLテンプレートが必要なメソッドである
  • 適切なパスにSQLファイルが配置されている、またはSQLアノテーションが付与されている
class SqlFileExistInspectionVisitor(
    private val holder: ProblemsHolder,
    private val shortName: String,
) : JavaElementVisitor() {

    // メソッド定義要素への処理
    override fun visitMethod(method: PsiMethod) {
        super.visitMethod(method)
        // チェック対象のファイルか
        val file = method.containingFile
        if (!isJavaOrKotlinFileType(file) || getDaoClass(file) == null) return

        // チェックロジックの実装
        val psiDaoMethod = PsiDaoMethod(method.project, method)
        if (psiDaoMethod.isUseSqlFileMethod()) {
            checkDaoMethod(psiDaoMethod)
        }
    }

    private fun checkDaoMethod(psiDaoMethod: PsiDaoMethod) {
        val identifier = psiDaoMethod.psiMethod.nameIdentifier ?: return

        // SQLファイルがない場合、エラーハイライトする
        if (psiDaoMethod.sqlFile == null) {
            ValidationSqlFileExistResult(
                psiDaoMethod,
                identifier,
                shortName,
            ).highlightElement(
                holder,
            )
        }
    }
}

コンストラクタパラメータ

サンプルコードでは、Visitorクラス内でエラーハイライト用のオブジェクトを作成し、
インスペクションクラスから渡されたProblemsHolderに格納する処理を行います。

デフォルトでエラーレベルを設定していますが、識別名shortNameを受け取り、ハイライトの種類をIntelliJの設定から判定するための処理で利用します。

class SqlFileExistInspectionVisitor(
    // エラーハイライトオブジェクトを格納する
    private val holder: ProblemsHolder,
    // エラーレベルを設定から取得するためのインスペクション識別名
    private val shortName: String, 
) : JavaElementVisitor() 

検査対象のチェック

以下のオーバーライドメソッドで、メソッド定義要素への処理を実装します。
まずはチェック対象のファイルであるかを検証し、対象でなければ後続処理には入りません。

対象のチェック方法については、以前にもご紹介したファイルタイプチェック実装などを参考にしてください。

IntelliJプラグイン開発の始め方~アクション機能編~

// メソッド定義要素への処理
override fun visitMethod(method: PsiMethod) {
    super.visitMethod(method)
    // チェック対象のファイルか
    val file = method.containingFile
    if (!isJavaOrKotlinFileType(file) || getDaoClass(file) == null) return

    // チェックロジックの実装
    val psiDaoMethod = PsiDaoMethod(method.project, method)
    if (psiDaoMethod.isUseSqlFileMethod()) {
        checkDaoMethod(psiDaoMethod)
    }
}

アノテーション付与のチェック

メソッドに付与されているアノテーションが、SQLテンプレートを必要としていることをチェックします。
メソッドアノテーション要素の判定は、以下のように実装できます。

独自に用意しているPsiDaoMethodクラス経由でアノテーションタイプを取得し、
最終的にSQLテンプレートが必要なアノテーションであるかを判定しています。

fun getPsiAnnotation(element: PsiModifierListOwner): PsiAnnotation? =
    AnnotationUtil.findAnnotation(
        element, // PsiMethod(メソッド定義要素)
        annotationName, // 取得したいアノテーションのクラス名 
    )

SQLテンプレートが必要なメソッドであることを確認出来たら、次にSQLファイルの検索を行います。

SQLファイルの検索

DAOメソッドのファイルパス、メソッド名を基にSQLファイルが適切に配置されていることをチェックします。

PsiDaoMethodクラスの処理で取得した、リソースディレクトリ内にあるSQLファイル情報を保持しています。
この部分はアクション機能の実装と同様のロジックとなりますので、以下の記事も参考にしてみてください。

IntelliJプラグイン開発の始め方~アクション機能編~

private fun checkDaoMethod(psiDaoMethod: PsiDaoMethod) {
    val identifier = psiDaoMethod.psiMethod.nameIdentifier ?: return
    // SQLファイルがない場合、エラーハイライトする
    if (psiDaoMethod.sqlFile == null) {
        ValidationSqlFileExistResult(
            psiDaoMethod,
            identifier,
            shortName,
        ).highlightElement(
            holder,
        )
    }
}

エラーハイライト

独自に用意しているValidationSqlFileExistResultクラス内では、holderにエラー情報を登録する処理を行っています。
この登録処理を行うことで、エディタ上にエラーハイライトが表示されるようになります。

エラー情報登録処理部分のみのサンプルを記載します。(以下はhighlightElement()によって呼び出される処理です)。

// ValidationSqlFileExistResultでのコンストラクタで受け取ったidentifyをエラーハイライト対象にする
val project = psiDaoMethod.psiMethod.project
holder.registerProblem(
    identify, // エラーハイライトする要素
    MessageBundle.message("inspection.invalid.dao.notExistSql"), // エラーメッセージ
    problemHighlightType(project, shortName), // エラーレベルを取得して設定
    highlightRange, // エラーハイライトするTextRange
)

problemHighlightType()では、インスペクション識別名を基にIntelliJの設定からエラーレベルを取得します。

// エラーレベルを設定から取得
private fun getInspectionErrorLevel(
    project: Project,
    inspectionShortName: String,
): HighlightDisplayLevel? {
    val profileManager = InspectionProfileManager.getInstance(project)
    val currentProfile = profileManager.currentProfile
    val toolState: ToolsImpl? = currentProfile.getToolsOrNull(inspectionShortName, project)
    return toolState?.level
}

protected fun problemHighlightType(
    project: Project,
    shortName: String,
) = when ( // 設定から取得したエラーレベルによって、ハイライト設定を返す
            getInspectionErrorLevel(
                project,
                shortName,
            )
        ) {
            HighlightDisplayLevel.Companion.ERROR -> ProblemHighlightType.ERROR
            HighlightDisplayLevel.Companion.WARNING -> ProblemHighlightType.WARNING
            else -> ProblemHighlightType.WARNING
        }

【補足】

エラーに対してクイックフィックスを提案したい場合、holder.registerProblem()にクイックフィックスオブジェクトを渡します。
クイックフィックスの実装は、任意のファクトリクラスを作成してLocalQuickFixサブクラスを返すようにします。
詳細は公式ドキュメントをご参照ください。

インスペクション機能の説明HTML

実装したインスペクション機能は、IntelliJの設定画面に追加されます。
この設定画面で表示される詳細な機能説明を、HTMLファイルに記載します。
HTMLファイルはリソースディレクトリのinspectionDescriptionsフォルダに配置してください。

InspectionHtml

今回は簡潔に文章のみの説明をしていますが、<code>タグを使うことでコードブロックの記述も可能です。

<html lang="en">
<body>
<p>
  This inspection ensures that a DAO method has a corresponding SQL file.
  If the SQL file is missing, a warning or error is displayed, and a quick-fix is provided to generate the missing file.
</p>
</body>
</html>

【補足】
インスペクションクラスで表示されるファイルアイコンをクリックすることで、対応するHTMLファイルの作成、表示が可能です。
InspectionDescription

動かしてみる

コード検査機能で、SQLファイルのないDAOメソッドがエラーハイライトされることを確認してみましょう!

デバッグ起動方法は前回と同じく、デバッグ起動するで実行してください。

参考::「Doma Tools」実装コード

Doma Tools」の同様のインスペクション機能は以下コードで実装しています。

SqlFileExistInspectionVisitor

まとめ

コード検査機能の実装のポイントは、以下4点になります。

  • コード検査のように状態監視を行う機能では、プロバイダークラスを起点にした処理を行う
  • インスペクションクラスやHTMLで、検査機能の情報を定義する
  • メインのチェック処理はVisitorクラスで担当する
  • どの要素がどのPSI要素クラスとして認識されるかを確認しておく

一部のIntelliJ機能の実装では複数のクラスを用意する必要がありますが、
クラスごとに適切な役割を持たせることが保守性を高めるポイントになります。

またIntelliJプラグインのコードの解析を伴う処理において、
PSI要素クラスの利用が必須となりますが、コード検査機能の実装は大変良い学習機会になると思います。

さいごに

今回はコード検査機能の実装についてご紹介しました。

コード検査はビルド前にエラーを検知できることができ、開発効率化や実装者の負担を減らせる有用な機能です。
ご紹介できなかったクイックフィックスの実装などにも挑戦して、より良い開発体験を実現してみてください。

レビューも投稿していただけますと大変励みになります🙇‍♀️

Doma Tools マーケットプレースページ

プラグインOSSとしてDomaコミュニティへ寄贈されています。
不具合修正や機能要望、ディスカッションにもぜひご参加ください。

採用ページ

執筆:@terao.haruka
レビュー:@nakamura.toshihiro
Shodoで執筆されました