Retty Tech Blog

実名口コミグルメサービスRettyのエンジニアによるTech Blogです。プロダクト開発にまつわるナレッジをアウトプットして、世の中がHappyになっていくようなコンテンツを発信します。

ChatGPTに特定業務特化IntelliJプラグインを作らせる

この記事はRetty Advent Calendarの2日目の記事です。

はじめに

Rettyでエンジニアリングマネージャーを務める山田です。

ChatGPTが登場してから一年が過ぎました。その後社会にはAIが広まりつつあり、コーディングの世界にもGitHub CopilotのようなAIを応用した製品が浸透してきています。既に利用されているという方も多いのではないでしょうか? AIを活用してのコーディングはとても快適で、お願いした通りにコードを作成してくれたり、よくわからないコードを説明してくれたり、もはやこれ無しでの仕事というのは考えられなくなってきています。

では、AIがあれば人間は面倒なコードリーディングを行わなくて済むのでしょうか?そのような未来もそう遠くは無いでしょうが、現状はそうではないと私は思っています。 Rettyにおいてもそうですが、10年以上保たれてきたサービスを構成するコード量は数十万行にも及びます。悲しいことにこうした巨大なコードベースが息づく世界ではトークン量の制限が枷となり、AIに仕事をお任せすることが難しくなってしまいます。 さらに、ドメイン知識が十分にコードに反映されておらず、外部のドキュメント、あるいは暗黙知を参照しなければわからないことも数多いです。こうした仕事はAIにはまだまだ荷が重いです。

ということで、特にこういったサービスを対象とした考古学、もとい、コードリーディングを通した現状調査は人間の仕事になります。しかし、これだけAIが流行っている世の中ですから、こうした仕事もできる限りAIの助けを借りて楽をしたくなります。
特に私の場合EM業の傍らで調査を進める必要があったため、楽に、短時間に事を済ませたいモチベがありました。そんな時にコードリーディングを補佐してくれる業務に特化した専用IntelliJプラグインをAIに作って貰って楽をすれば良いんだ!と思いつきます。

そこで、本記事では架空のプロジェクトのコードリーディング妨げる課題を対象とし、これを解決するIntelliJプラグインをChatGPTに作らせてみます。

前提

本記事では以下の環境で動作を確認しています。

  • IntelliJ IDEA 2023.2.5 Ultimate Edition
  • LivePlugin 0.8.6

plugins.jetbrains.com

LivePluginはIDEの再起動無しにプラグインのロードを可能にするなど、IntelliJプラグイン開発を簡単にするプラグインです。 なお、分量が多くなってしまうため、Live Pluginの詳細な利用方法については本記事では触れません。GitHubのREADMEにある程度記載されているので、こちらを参照いただければと思います。

GitHub - dkandalov/live-plugin: IntelliJ plugin for writing IntelliJ plugins at runtime ⚡️

また、ChatGPTではモデルとしてGPT-4を利用しています。

課題の説明

以下のようなPHPを利用したコードが存在しています。

<?php
class Calculator {
    public function add($a, $b) {
        return $a + $b;
    }
}

class ResultEditor {
    // メソッドの結果に"Hello!"を付与する
    static function addHello($class, $method, $params) {
        $class_obj = new $class();
        return "Hello!" . call_user_func_array(array($class_obj, $method), $params);
    }
}

echo ResultEditor::addHello('Calculator', 'add', array(1, 2)); // => Hello!3

このプロジェクトでは様々な箇所でメソッドの結果に"Hello!"とつける必要があります。各所で毎回文字列の付与を行うのは手間なので、これを削減するために、呼び出したいメソッドを文字列で指定することでcall_user_func_arrayを利用して該当のメソッドを実行、その結果に"Hello!"をつけて返すResultEditor::addHelloが用意されており、これが多くの箇所でに利用されています。 しかし、ResultEditor::addHelloを介したメソッド呼び出しは文字列を用いて解決されているため、コードを解析しても呼び出し先のメソッドが参照されているかが分からず、IDEを利用してもコードジャンプを実行することができません。このような場合には都度文字列検索を行う必要があり不便です。 通常の参照を解決できるメソッド呼び出しと同じく、こういったプロジェクト内で多用されている特殊なケースに対してもコードジャンプを実現してくれるプラグインがあるとコードリーディングがしやすくなりそうです。

ChatGPTと会話

コードの実装をさせてみる

「課題の説明」のテキストと大体同じような内容のテキストに、所望のプラグインの挙動の説明を与えてChatGPTに問い合わせてみます。本来はもう少しプロンプトを整理しておくのが望ましいですが、雑な文章でも意を汲み取ってくれるか気になるので、あえてこのままやってみます。送信したプロンプトは以下のようになりました。

```php
class Calculator {
    public function add($a, $b) {
        return $a + $b;
    }
}

class ResultEditor {
    static function addHello($class, $method, $params) {
        $class_obj = new $class();
        return "Hello!" . call_user_func_array(array($class_obj, $method), $params);
    }
}

echo ResultEditor::addHello('Calculator', 'add', array(1, 2)); // => Hello!3
```

このPHPを利用したプロジェクトでは様々な箇所でメソッドの結果に"Hello!"とつける必要があり、`ResultEditor::addHello`を利用したメソッド呼び出しが大量に存在しています。
しかし、`ResultEditor::addHello`を介したメソッド呼び出しは文字列を用いて解決されているため、IDEを利用しても直接コードジャンプすることができません。都度文字列検索を行う必要があり不便です。
通常のメソッドと同じく、こういった場合にもコードジャンプを実現してくれるプラグインが欲しいです。

私はIntelliJを利用しています。プラグインの開発・動作にはLivePluginを利用します。この前提で、`addHello`にカーソルを当てている際にAlt + Enterのショートカットにより表示されるメニューから「Navigate to target method」という項目を選択すると呼び出し先のメソッドにジャンプするようなインターフェースで実現して欲しいです。

これに対して以下のような応答が返ってきました。

IntelliJ IDEAでLivePluginを使用してカスタムプラグインを開発する場合、指定された要件に沿った機能を実装するには、いくつかのステップを踏む必要があります。以下に、基本的なアプローチを説明します。

### ステップ1: LivePlugin セットアップ

1. IntelliJ IDEAにLivePluginをインストールします。
2. LivePlugin用のプロジェクトを作成します。

~~~長いので省略~~~

### 注意点

[プラグインにまつわる一般的な注意事項]

これは基本的なガイドラインです。具体的な実装には、IntelliJのAPIと内部構造に関するより深い知識と理解が必要です。また、プラグイン開発に関するIntelliJの公式ドキュメントやコミュニティフォーラムを参照することをお勧めします。

長いので一部省略しましたが、プラグインの作り方について教えてくれました。少なくともIntelliJプラグインを作りたいという意図は伝わったようなので、コードを作成してもらうようにお願いしてみます。

なお、ここで一つポイントがあり、今回はPsiElementBaseIntentionActionを利用しての実装をお願いしています。InetlliJのプラグイン実装についての知識は学習データにもそんなに多くないようで、多少人間側から知識を補助してあげないと動作するプラグインが作られませんでした。 今回の場合はPsiElementBaseIntentionActionを実装したクラスのインスタンスをLivePluginの提供するメソッドである registerIntentionで登録して動作させるアプローチを取るため、PsiElementBaseIntentionActionを利用した実装をお願いするようにします。このように、GPT-4をそのまま使うと人間にもIntelliJプラグイン実装にまつわる専門知識が求められるところではあるので、理想的にはGPTsでこの辺りのドキュメントを投げ込んだオレオレプラグイン制作AIを作っておくのが良いのだろうとは思います。

以上からコードの実装をお願いするプロンプトとして以下のようなテキストを送信しました。

LivePluginはインストールしました。プラグインを構成するのに必要なコードを全て生成してください。

プラグインの実装にはPsiElementBaseIntentionActionを利用してください。言語はKotlinを利用してください

これに対して以下のような応答が返ります。

LivePluginを使用してIntelliJ IDEA用のカスタムプラグインを作成するために、`PsiElementBaseIntentionAction`を使用するKotlinコードを以下に示します。このプラグインは、`ResultEditor::addHello`メソッドを検出し、関連するターゲットメソッドにジャンプする機能を提供します。

### カスタムインテンションアクションの作成

1. **プラグインの基本構成**: `PsiElementBaseIntentionAction`を継承したクラスを作成します。

```kotlin
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.IncorrectOperationException

class NavigateToTargetMethodAction : PsiElementBaseIntentionAction() {
    override fun getText(): String = "Navigate to target method"
    override fun getFamilyName(): String = "NavigateToTargetMethod"
    override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
        // メソッド呼び出しの検出ロジックを実装
    }

    @Throws(IncorrectOperationException::class)
    override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
        // ジャンプロジックを実装
    }

    override fun startInWriteAction(): Boolean = false
}
```

2. **利用可能性の判定**: `isAvailable`メソッドで、カーソルが`ResultEditor::addHello`メソッドの呼び出し上にあるかどうかを判定します。

```kotlin
override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
    val methodCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression::class.java)
    return methodCall?.methodExpression?.text == "ResultEditor::addHello"
}
```

3. **メソッドにジャンプするロジック**: `invoke`メソッド内で、関連するメソッドにジャンプするロジックを実装します。

```kotlin
@Throws(IncorrectOperationException::class)
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
    val methodCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression::class.java)
    val targetMethodName = methodCall?.argumentList?.expressions?.get(1)?.text ?: return

    // ターゲットメソッドにジャンプするロジックを実装
}
```

### プラグインのテストとデバッグ

~~~以下略~~~

それっぽいコードが出力されました。しかし、一部のロジックについてはコメントで済まされてしまっています。ここも実装してもらいましょう。

invokeメソッドの全体のコードを出力してください
`invoke` メソッドの実装は、`ResultEditor::addHello` メソッドの呼び出しを解析して、関連するターゲットメソッドにジャンプするためのロジックを含む必要があります。以下は、この機能を実装するための基本的なアプローチです。

```kotlin
@Throws(IncorrectOperationException::class)
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
    val methodCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression::class.java)
    val targetClassName = methodCall?.argumentList?.expressions?.get(0)?.text?.removeSurrounding("\"") ?: return
    val targetMethodName = methodCall?.argumentList?.expressions?.get(1)?.text?.removeSurrounding("\"") ?: return

    // ターゲットクラスとメソッドの検索
    val psiClass = JavaPsiFacade.getInstance(project).findClass(targetClassName, GlobalSearchScope.allScope(project))
    val targetMethod = psiClass?.methods?.firstOrNull { it.name == targetMethodName }

    // ターゲットメソッドにジャンプ
    targetMethod?.navigate(true)
}
```

このコードは以下の処理を行います
~~~以下略~~~

これでコードの全体像が見えてきました。現状のコードをエディタに入力すると、以下のようになります。

import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.IncorrectOperationException

class NavigateToTargetMethodAction : PsiElementBaseIntentionAction() {
    override fun getText(): String = "Navigate to target method"
    override fun getFamilyName(): String = "NavigateToTargetMethod"
    override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
        val methodCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression::class.java)
        return methodCall?.methodExpression?.text == "ResultEditor::addHello"
    }

    @Throws(IncorrectOperationException::class)
    override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
        val methodCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression::class.java)
        val targetClassName = methodCall?.argumentList?.expressions?.get(0)?.text?.removeSurrounding("\"") ?: return
        val targetMethodName = methodCall?.argumentList?.expressions?.get(1)?.text?.removeSurrounding("\"") ?: return

        // ターゲットクラスとメソッドの検索
        val psiClass = JavaPsiFacade.getInstance(project).findClass(targetClassName, GlobalSearchScope.allScope(project))
        val targetMethod = psiClass?.methods?.firstOrNull { it.name == targetMethodName }

        // ターゲットメソッドにジャンプ
        targetMethod?.navigate(true)
    }

    override fun startInWriteAction(): Boolean = false
}

一見それっぽいコードが作られていることが分かります。

コードの問題点を修正させる

それらしいコードはできましたが、これにはいくつかの問題があります。例えば、JavaPsiFacadeを利用している点です。今回対象とするコードはPHPのコードのはずです。 こうしたおかしなところをChatGPTに突っ込んでいきましょう。

JavaPsiFacadeとありますが、対象としているコードはPHPによるものです。これで正しいですか?PHPに対応したコードを出力して欲しいです
申し訳ありません、PHPコードの解析にはJavaPsiFacadeではなく、PHP固有のクラスを使用する必要があります。IntelliJ IDEAのPHPプラグインは、PHP固有のPsi要素を提供します。以下は、PHPコードに適した`invoke`メソッドの実装です。

```kotlin
@Throws(IncorrectOperationException::class)
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
    val methodCall = PsiTreeUtil.getParentOfType(element, PhpCall::class.java)
    if (methodCall == null || methodCall.parameters.isEmpty()) return

    val targetMethodName = methodCall.parameters[1].text.trim('\'', '"')
    val targetClassName = methodCall.parameters[0].text.trim('\'', '"')

    // PHPクラスとメソッドの検索
    val phpIndex = PhpIndex.getInstance(project)
    val classes = phpIndex.getClassesByFQN(targetClassName)
    val targetMethod = classes
        .flatMap { it.methods }
        .firstOrNull { it.name == targetMethodName }

    // ターゲットメソッドにジャンプ
    targetMethod?.navigate(true)
}
```

このコードは以下の処理を行います:

~~~以下略~~~

PHPに対応したコードになりました。上記のコードは正しそうですが、エディタに入力するとPhpIndexの依存解決ができないことが分かります。 この問題を修正する知識も今のところはChatGPTが持ち合わせていないため、人が頑張る必要があります。この依存を解決するにはLivePluginの機能を用いて依存を記述する必要があります。 今回のようなPHPプラグインに依存する機能を利用する場合は、どこでも良いので以下のようなコメントをプラグインのコード上に入力する必要があります。

// depends-on-plugin com.jetbrains.php

このコメントにより依存解決ができるようになるため、エラーとなっているPhpIndex上でAlt + Enterを実行することで、適切なimport文が挿入され依存が正しく解決されるようになります。

ここまで来るとあともう少しです。PsiMethodCallExpressionPhpCallの依存解決に失敗しているエラーが残っている状態になっていると思います。これらもChatGPTにお任せして解決してもらいましょう。

まずはPsiMethodCallExpressionについて問い合わせてみましょう。

val methodCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression::class.java)
この箇所で以下のようなエラーが発生します。対応方法を教えてください
Unresolved reference: PsiMethodCallExpression
`Unresolved reference: PsiMethodCallExpression` というエラーは、`PsiMethodCallExpression` クラスが見つからない、つまりそのクラスに対する参照が解決できないことを意味します。このエラーは主に、必要なライブラリがプロジェクトのクラスパスに含まれていない場合に発生します。

ただし、`PsiMethodCallExpression` はJava固有のクラスであり、PHPのコードを解析する場合には適切ではありません。PHPのメソッド呼び出しを取得するためには、PHP固有のPsiクラスを使用する必要があります。PHPのメソッド呼び出しは、`PhpPsiElementFactory`や`MethodReference`などのクラスを使用して取得できます。

以下のステップに従って修正してください:

1. **適切なクラスの使用**: PHPのメソッド呼び出しを取得するために、`MethodReference` や他のPHP固有のPsiクラスを使用します。

2. **依存関係の確認**: IntelliJ Platform SDKを使用している場合、PHP言語のサポートを提供する依存関係(PHPプラグインのライブラリやモジュール)がプロジェクトに含まれていることを確認してください。

3. **適切なメソッドの使用**: PHPのコードを解析するために、Java言語のPsiクラスではなく、PHP言語に特化したメソッドを使用します。

例えば、以下のように変更することができます:

```kotlin
val methodCall = PsiTreeUtil.getParentOfType(element, MethodReference::class.java)
```

ここで、`MethodReference` はPHPのメソッド呼び出しを表すクラスです。このクラスはPHPプラグインによって提供されますので、このクラスを使用するためにはPHPプラグインのAPIに依存していることを確認し、適切に依存関係を設定する必要があります。

これらの変更を行った後でもエラーが解消されない場合は、依存関係やプロジェクトの設定を再確認してください。また、IntelliJ IDEAの公式ドキュメントやコミュニティフォーラムを利用して、さらなるサポートを得ることも検討してください。

最後にPhpCallについてです。

Unresolved reference: methodExpressionは解決しました。
しかし以下の箇所でエラーが発生します
val methodCall = PsiTreeUtil.getParentOfType(element, PhpCall::class.java)
エラーの内容は次の通りです。
Unresolved reference: PhpCall
対応をお願いします
`Unresolved reference: PhpCall` エラーは、`PhpCall` というクラスが見つからないことを意味します。これは、PHPのコード解析に関連する正しいクラスを使用していない可能性があります。PHPのコード解析には `com.jetbrains.php.lang.psi.elements.MethodReference` クラスを使用する必要があります。

`MethodReference` はPHPのメソッド呼び出しを表すクラスです。このクラスを使って、`ResultEditor::addHello` メソッドの呼び出しを見つけることができます。以下に修正したコードの例を示します。

```kotlin
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.intellij.psi.util.PsiTreeUtil

// ...

override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
    val methodReference = PsiTreeUtil.getParentOfType(element, MethodReference::class.java)
    if (methodReference == null) return false

    val isTargetMethodCall = methodReference.name == "addHello" &&
                             methodReference.classReference?.name == "ResultEditor"
    return isTargetMethodCall
}

@Throws(IncorrectOperationException::class)
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
    val methodReference = PsiTreeUtil.getParentOfType(element, MethodReference::class.java) ?: return

    // ここで必要なロジックを実装
    // 例: ターゲットクラス名とメソッド名の取得、適切なメソッドへのジャンプなど
}
```

### 注意点
~~~以下略~~~

ここまで出力されたコードをまとめ、IntelliJの補完機能でimportも済ませると、以下のようなコードになります。 なお、前述した通り、registerIntentionを利用するため、このメソッドの呼び出しも追加されている点に注意してください。

import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.IncorrectOperationException
import com.jetbrains.php.PhpIndex
import com.jetbrains.php.lang.psi.elements.MethodReference
import liveplugin.registerIntention

// depends-on-plugin com.jetbrains.php

registerIntention(NavigateToTargetMethodAction())

class NavigateToTargetMethodAction : PsiElementBaseIntentionAction() {
    override fun getText(): String = "Navigate to target method"
    override fun getFamilyName(): String = "NavigateToTargetMethod"
    override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
        val methodReference = PsiTreeUtil.getParentOfType(element, MethodReference::class.java)
        if (methodReference == null) return false

        val isTargetMethodCall = methodReference.name == "addHello" &&
            methodReference.classReference?.name == "ResultEditor"
        return isTargetMethodCall
    }

    @Throws(IncorrectOperationException::class)
    override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
        val methodCall = PsiTreeUtil.getParentOfType(element, MethodReference::class.java)
        if (methodCall == null || methodCall.parameters.isEmpty()) return

        val targetMethodName = methodCall.parameters[1].text.trim('\'', '"')
        val targetClassName = methodCall.parameters[0].text.trim('\'', '"')

        // PHPクラスとメソッドの検索
        val phpIndex = PhpIndex.getInstance(project)
        val classes = phpIndex.getClassesByFQN(targetClassName)
        val targetMethod = classes
            .flatMap { it.methods }
            .firstOrNull { it.name == targetMethodName }

        // ターゲットメソッドにジャンプ
        targetMethod?.navigate(true)
    }

    override fun startInWriteAction(): Boolean = false
}

この時点のコードでようやくエラーも全て解消され、動作するものになりました。一部人間の手による追記が入ってますが、9割くらいはAIに全てお任せです。エラーの解消もAIがほぼやってくれました。あまり情報がないIntelliJプラグイン開発にそのまま挑戦するとなかなか大変なのですが、だいぶ楽ができました。

完成したコードを動かしてみる

ここまでできたら実際にコードを動かしてみましょう!

はじめにプラグインを有効にしていない状態ではAlt + Enterを実行しても何も起こりません。 今回作成したプラグインを有効にしてから再度Alt + Enterを実行することで、メニューが表示されるようになりました。ここから新規に追加された「Navigate to target method」を選択することで文字列で表現されていたメソッドへジャンプすることができました! 当初欲しかった機能が概ね実現できました。

ChatGPTにIntelliJプラグインを作らせる際のポイント

ChatGPTにIntelliJプラグインを作らせることには成功しましたが、完全にお任せでできたわけではなく、いくつか人間の手による補助が必要でした。このポイントを一通りまとめてみましょう。

  • 実装方針を決めて指示する
    • プラグインに要求する機能に合わせて、どのようなクラスを使うべきなのかの指示を行いましょう。今回はソースの構造に応じてAlt+Enterで表示されるメニュー(Intentionと呼ぶらしい)に機能を追加して欲しかったので、PsiElementBaseIntentionActionの使用を指示しました。
  • Live Pluginを利用する場合、別プラグインへの依存はLivePlugin固有の記法で記述する必要がある
  • Live Pluginを使用する場合、registerIntentionなどを用いて自力でActionの登録を行う必要がある

後者二つについてはLive Plugin絡みの知識が必要なものですが、ただでさえニッチなIntelliJプラグイン制作のうち、さらにニッチな話になってしまうところなので致し方ないのかないところはありそうです。こうした特性を理解すると快適にプラグインを作らせることができるようになります。

おわりに

多少人間の補助が必要なところもありましたがIntelliJプラグイン実装のような、とてもニッチな領域においてもChatGPTの手を借りることができることが分かりました。 今回は特定のコードリーディングにおける課題の解消を実現しましたが、プラグインでできることは非常に多く、そのほかの多くの事柄で同様にしてChatGPTを活用しての業務効率化を行えそうです。 うまく作成できたプラグインをチームメンバーにも共有することで、チーム単位で生産性を大きく改善することも夢ではないです。 こんな業務に特化したプラグインがあれば仕事が捗るのになーとお悩みの方は、ぜひ一度ChatGPTに問い合わせてみてください! 機会があれば文中でも触れていたGPTsを用いたプラグイン制作用のチャットAI作りもやってみようと思います。

最後に、出力が長くて省略している箇所も多かったChatGPTとのやりとり全文を以下にそのまま載せておきます。

ChatGPTとのやりとり全文

Q = ユーザーからの入力 A = ChatGPTからの応答

Q:

```php
class Calculator {
    public function add($a, $b) {
        return $a + $b;
    }
}

class ResultEditor {
    static function addHello($class, $method, $params) {
        $class_obj = new $class();
        return "Hello!" . call_user_func_array(array($class_obj, $method), $params);
    }
}

echo ResultEditor::addHello('Calculator', 'add', array(1, 2)); // => Hello!3
```

このPHPを利用したプロジェクトでは様々な箇所でメソッドの結果に"Hello!"とつける必要があり、`ResultEditor::addHello`を利用したメソッド呼び出しが大量に存在しています。
しかし、`ResultEditor::addHello`を介したメソッド呼び出しは文字列を用いて解決されているため、IDEを利用しても直接コードジャンプすることができません。都度文字列検索を行う必要があり不便です。
通常のメソッドと同じく、こういった場合にもコードジャンプを実現してくれるプラグインが欲しいです。

私はIntelliJを利用しています。プラグインの開発・動作にはLivePluginを利用します。この前提で、`addHello`にカーソルを当てている際にAlt + Enterのショートカットにより表示されるメニューから「Navigate to target method」という項目を選択すると呼び出し先のメソッドにジャンプするようなインターフェースで実現して欲しいです。

A:

IntelliJ IDEAでLivePluginを使用してカスタムプラグインを開発する場合、指定された要件に沿った機能を実装するには、いくつかのステップを踏む必要があります。以下に、基本的なアプローチを説明します。

### ステップ1: LivePlugin セットアップ

1. IntelliJ IDEAにLivePluginをインストールします。
2. LivePlugin用のプロジェクトを作成します。

### ステップ2: アクションリスナーの作成

カスタムアクションを定義し、Alt + Enterのショートカットに対応させます。このアクションは、カーソルが`addHello`メソッドにあるときに特定のコンテキストメニュー項目を表示します。

```kotlin
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil

class NavigateToTargetMethodAction : AnAction() {
    override fun actionPerformed(e: AnActionEvent) {
        val editor: Editor = e.getRequiredData(CommonDataKeys.EDITOR)
        val file: PsiFile = e.getRequiredData(CommonDataKeys.PSI_FILE)
        // メソッド検出とジャンプのロジックを実装
    }

    override fun update(e: AnActionEvent) {
        // コンテキストメニュー表示の条件を指定
    }
}
```

### ステップ3: カーソル位置のメソッド検出

カーソルの位置に基づいて、`addHello`メソッドの使用を検出し、関連するクラスとメソッド名を抽出します。

### ステップ4: ターゲットメソッドへのジャンプ

抽出したメソッド名を使用して、ターゲットメソッドのソースにジャンプするロジックを実装します。これは、IDEの内部APIを使用して、ソースファイル内の特定のメソッド定義に焦点を当てることができます。

### ステップ5: プラグインのテストとデバッグ

開発したプラグインをテストし、必要に応じてデバッグします。

### 注意点

- IntelliJのAPIと内部構造を理解することが重要です。
- LivePluginはKotlinまたはGroovyでの開発をサポートしています。
- ドキュメントとコミュニティリソースを活用して、具体的な実装の詳細を理解します。

これは基本的なガイドラインです。具体的な実装には、IntelliJのAPIと内部構造に関するより深い知識と理解が必要です。また、プラグイン開発に関するIntelliJの公式ドキュメントやコミュニティフォーラムを参照することをお勧めします。

Q:

LivePluginはインストールしました。プラグインを構成するのに必要なコードを全て生成してください。

プラグインの実装にはPsiElementBaseIntentionActionを利用してください。言語はKotlinを利用してください

A:

LivePluginを使用してIntelliJ IDEA用のカスタムプラグインを作成するために、`PsiElementBaseIntentionAction`を使用するKotlinコードを以下に示します。このプラグインは、`ResultEditor::addHello`メソッドを検出し、関連するターゲットメソッドにジャンプする機能を提供します。

### カスタムインテンションアクションの作成

1. **プラグインの基本構成**: `PsiElementBaseIntentionAction`を継承したクラスを作成します。

```kotlin
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.IncorrectOperationException

class NavigateToTargetMethodAction : PsiElementBaseIntentionAction() {
    override fun getText(): String = "Navigate to target method"
    override fun getFamilyName(): String = "NavigateToTargetMethod"
    override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
        // メソッド呼び出しの検出ロジックを実装
    }

    @Throws(IncorrectOperationException::class)
    override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
        // ジャンプロジックを実装
    }

    override fun startInWriteAction(): Boolean = false
}
```

2. **利用可能性の判定**: `isAvailable`メソッドで、カーソルが`ResultEditor::addHello`メソッドの呼び出し上にあるかどうかを判定します。

```kotlin
override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
    val methodCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression::class.java)
    return methodCall?.methodExpression?.text == "ResultEditor::addHello"
}
```

3. **メソッドにジャンプするロジック**: `invoke`メソッド内で、関連するメソッドにジャンプするロジックを実装します。

```kotlin
@Throws(IncorrectOperationException::class)
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
    val methodCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression::class.java)
    val targetMethodName = methodCall?.argumentList?.expressions?.get(1)?.text ?: return

    // ターゲットメソッドにジャンプするロジックを実装
}
```

### プラグインのテストとデバッグ

- LivePluginを使って、このコードをIDEAに組み込み、動作をテストします。
- コードは`ResultEditor::addHello`メソッドの使用を検出し、関連するターゲットメソッドにジャンプします。

### 注意点

- このコードは基本的なガイドラインを提供しますが、プロジェクトの特定のニーズや構造に応じて調整が必要です。
- IDEのAPIと内部構造に関する深い理解が必要です。
- IntelliJの公式ドキュメントやコミュニティリソースを活用して、さらに詳しい情報を得てください。

この基本的なガイドラインを元に、プロジェクト固有のニーズに合わせてカスタマイズすることが重要です。

Q

invokeメソッドの全体のコードを出力してください

A:

`invoke` メソッドの実装は、`ResultEditor::addHello` メソッドの呼び出しを解析して、関連するターゲットメソッドにジャンプするためのロジックを含む必要があります。以下は、この機能を実装するための基本的なアプローチです。

```kotlin
@Throws(IncorrectOperationException::class)
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
    val methodCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression::class.java)
    val targetClassName = methodCall?.argumentList?.expressions?.get(0)?.text?.removeSurrounding("\"") ?: return
    val targetMethodName = methodCall?.argumentList?.expressions?.get(1)?.text?.removeSurrounding("\"") ?: return

    // ターゲットクラスとメソッドの検索
    val psiClass = JavaPsiFacade.getInstance(project).findClass(targetClassName, GlobalSearchScope.allScope(project))
    val targetMethod = psiClass?.methods?.firstOrNull { it.name == targetMethodName }

    // ターゲットメソッドにジャンプ
    targetMethod?.navigate(true)
}
```

このコードは以下の処理を行います:

1. `methodCall` 変数は、カーソルの位置にある `PsiMethodCallExpression`(メソッド呼び出し)を取得します。

2. `targetClassName` と `targetMethodName` は、`addHello` メソッドの引数からターゲットのクラス名とメソッド名を取得します。

3. `JavaPsiFacade` を使用して、プロジェクト内で指定されたクラス名に一致するクラスを検索します。

4. クラス内で指定されたメソッド名に一致するメソッドを検索し、見つかった場合はそのメソッドにジャンプします。

### 注意点

- このコードは `addHello` メソッドが2つの引数(クラス名とメソッド名)を取ることを前提としています。プロジェクトの実装に応じて調整する必要があります。

- メソッド名やクラス名を取得する際に、引数が想定通りの形式であることを確認するための追加の検証が必要になる場合があります。

- `navigate(true)` はIDEAに組み込まれたナビゲーション機能を使用して、指定されたメソッドの場所にカーソルを移動します。

Q:

JavaPsiFacadeとありますが、対象としているコードはPHPによるものです。これで正しいですか?PHPに対応したコードを出力して欲しいです

A:

申し訳ありません、PHPコードの解析にはJavaPsiFacadeではなく、PHP固有のクラスを使用する必要があります。IntelliJ IDEAのPHPプラグインは、PHP固有のPsi要素を提供します。以下は、PHPコードに適した`invoke`メソッドの実装です。

```kotlin
@Throws(IncorrectOperationException::class)
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
    val methodCall = PsiTreeUtil.getParentOfType(element, PhpCall::class.java)
    if (methodCall == null || methodCall.parameters.isEmpty()) return

    val targetMethodName = methodCall.parameters[1].text.trim('\'', '"')
    val targetClassName = methodCall.parameters[0].text.trim('\'', '"')

    // PHPクラスとメソッドの検索
    val phpIndex = PhpIndex.getInstance(project)
    val classes = phpIndex.getClassesByFQN(targetClassName)
    val targetMethod = classes
        .flatMap { it.methods }
        .firstOrNull { it.name == targetMethodName }

    // ターゲットメソッドにジャンプ
    targetMethod?.navigate(true)
}
```

このコードは以下の処理を行います:

1. `methodCall` 変数は、カーソルの位置にある `PhpCall`(PHPのメソッド呼び出し)を取得します。

2. `targetMethodName` と `targetClassName` は、`addHello` メソッドの引数からターゲットのクラス名とメソッド名を取得します。

3. `PhpIndex` を使用して、プロジェクト内で指定されたクラス名に一致するPHPクラスを検索します。

4. 指定されたクラス内で、指定されたメソッド名に一致するメソッドを検索し、見つかった場合はそのメソッドにジャンプします。

### 注意点

- このコードは `addHello` メソッドが2つの引数(クラス名とメソッド名)を取ることを前提としています。プロジェクトの実装に応じて調整する必要があります。

- メソッド名やクラス名を取得する際に、引数が想定通りの形式であることを確認するための追加の検証が必要になる場合があります。

- `PhpIndex` はPHPのクラスと関数を検索するためのキークラスです。これにより、プロジェクト内の任意のPHPクラスやメソッドにアクセスできます。

Q:

val methodCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression::class.java)
この箇所で以下のようなエラーが発生します。対応方法を教えてください
Unresolved reference: PsiMethodCallExpression

A:

`Unresolved reference: PsiMethodCallExpression` というエラーは、`PsiMethodCallExpression` クラスが見つからない、つまりそのクラスに対する参照が解決できないことを意味します。このエラーは主に、必要なライブラリがプロジェクトのクラスパスに含まれていない場合に発生します。

ただし、`PsiMethodCallExpression` はJava固有のクラスであり、PHPのコードを解析する場合には適切ではありません。PHPのメソッド呼び出しを取得するためには、PHP固有のPsiクラスを使用する必要があります。PHPのメソッド呼び出しは、`PhpPsiElementFactory`や`MethodReference`などのクラスを使用して取得できます。

以下のステップに従って修正してください:

1. **適切なクラスの使用**: PHPのメソッド呼び出しを取得するために、`MethodReference` や他のPHP固有のPsiクラスを使用します。

2. **依存関係の確認**: IntelliJ Platform SDKを使用している場合、PHP言語のサポートを提供する依存関係(PHPプラグインのライブラリやモジュール)がプロジェクトに含まれていることを確認してください。

3. **適切なメソッドの使用**: PHPのコードを解析するために、Java言語のPsiクラスではなく、PHP言語に特化したメソッドを使用します。

例えば、以下のように変更することができます:

```kotlin
val methodCall = PsiTreeUtil.getParentOfType(element, MethodReference::class.java)
```

ここで、`MethodReference` はPHPのメソッド呼び出しを表すクラスです。このクラスはPHPプラグインによって提供されますので、このクラスを使用するためにはPHPプラグインのAPIに依存していることを確認し、適切に依存関係を設定する必要があります。

これらの変更を行った後でもエラーが解消されない場合は、依存関係やプロジェクトの設定を再確認してください。また、IntelliJ IDEAの公式ドキュメントやコミュニティフォーラムを利用して、さらなるサポートを得ることも検討してください。

Q:

Unresolved reference: methodExpressionは解決しました。
しかし以下の箇所でエラーが発生します
val methodCall = PsiTreeUtil.getParentOfType(element, PhpCall::class.java)
エラーの内容は次の通りです。
Unresolved reference: PhpCall
対応をお願いします

A:

`Unresolved reference: PhpCall` エラーは、`PhpCall` というクラスが見つからないことを意味します。これは、PHPのコード解析に関連する正しいクラスを使用していない可能性があります。PHPのコード解析には `com.jetbrains.php.lang.psi.elements.MethodReference` クラスを使用する必要があります。

`MethodReference` はPHPのメソッド呼び出しを表すクラスです。このクラスを使って、`ResultEditor::addHello` メソッドの呼び出しを見つけることができます。以下に修正したコードの例を示します。

```kotlin
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.intellij.psi.util.PsiTreeUtil

// ...

override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
    val methodReference = PsiTreeUtil.getParentOfType(element, MethodReference::class.java)
    if (methodReference == null) return false

    val isTargetMethodCall = methodReference.name == "addHello" &&
                             methodReference.classReference?.name == "ResultEditor"
    return isTargetMethodCall
}

@Throws(IncorrectOperationException::class)
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
    val methodReference = PsiTreeUtil.getParentOfType(element, MethodReference::class.java) ?: return

    // ここで必要なロジックを実装
    // 例: ターゲットクラス名とメソッド名の取得、適切なメソッドへのジャンプなど
}
```

### 注意点

- 必要に応じて `com.jetbrains.php.lang.psi.elements.MethodReference` と関連するPHP Psiクラスのインポートが必要です。

- このコードは、特定のメソッド参照 (`ResultEditor::addHello`) が存在するかをチェックし、その後の処理で必要な操作を行います。

- PhpStorm または IntelliJ IDEA の PHP プラグインがインストールされていることを確認してください。これらのIDEには、PHPコード解析に必要なPsiクラスが含まれています。

- 実際のプロジェクトやコードベースによっては、さらに詳細な調整が必要になる場合があります。