routes.rbに色々なルーティングを定義したRailsアプリを作ってみた

先日 Railroads という、Rails開発向けのIntelliJ Platform Pluginを作りました。
RubyMine 2023.3系から、rails routes を便利に扱える Railways プラグインが動かなくなったので、代替プラグイン Railroads を作りました - メモ的な思考的な

 
そのプラグインの動作確認をするために

  • rails routes を実行し、設定したルーティグがどのように表示されるか確認
  • rails routes の結果を元に、Railroadsプラグインでコントローラとメソッドが表示されているか確認

ができるRailsアプリが必要になりました。

 
そこで、 routes.rb に色々なルーティングを設定したRailsアプリを作ってみたことから、メモを残します。

 
なお、今回定義したRailsアプリのルーティングについては、 Railroads プラグインでの確認に加え、ブラウザでの表示 (http://localhost:3000/rails/info/routes) でも確認しています。
6.1 既存のルールを一覧表示する | Rails のルーティング - Railsガイド

 
目次

 

環境

 

今回実装したルーティング一覧

Railsガイドに書かれている各種ルーティングを実装してみました。
Rails のルーティング - Railsガイド

 
具体的には以下を実装しました。

 
以降は作ったときのメモです。

 

メモ

resource とresources は同じコントローラにマッピングされる

Railsガイドに記載がありました。

単数形リソースは複数形のコントローラに対応付けられます。これは、同じコントローラで単数形のルーティング(/account)と複数形のルーティング(/accounts/45)を両方使いたい場合を想定しているためです。従って、resource :photoとresources :photosのどちらも、単数形ルーティングと複数形ルーティングを両方作成し、同一のコントローラ(PhotosController)に割り当てられます。

2.5 単数形リソース | Rails のルーティング - Railsガイド

 

resource (単数形ルーティング) には :id がない

単数形ルーティングの用途は以下のため、 :id がないようです。

ユーザーがページを表示する際にidを一切参照しないリソースが使われることがあります。たとえば、/profileでは常に「現在ログインしているユーザー自身」のプロファイルを表示し、他のユーザーidを参照する必要がないとします。

2.5 単数形リソース | Rails のルーティング - Railsガイド

 

浅いネストという書き方がある

2.7.2 浅いネスト | Rails のルーティング - Railsガイド によると、こんな感じで書けるようです。

resources :parents, shallow: true do
  resources :children
end

 
ルーティングを確認してみたところ、Railsガイドの

コレクション(index/new/createのような、idを持たないもの)のアクションだけを親のスコープの下で生成するという手法があります

というルーティングになっていました。

 
また、 shallow について調べたところ、以下にまとまっている通り、賛否両論あるようでした。
Railsの”shallow(浅い)”ルーティングを理解する #Rails - Qiita

 

ルーティングにも concern がある

concern を使ってルーティングを書きます。

concern :image_attachable do
  resources :images, only: :index
end
resources :news, concerns: :image_attachable, only: :index

 
すると、ルーティングは以下になりました。

news の下に images というルーティングができる一方、コントローラは ImagesController になりました。

 

match + via の場合、rails routes の HTTP Verb が まとめて表示される

以下のRailsガイドにあるように、 match + via で定義した場合、 rails routes の表示が他と HTTP Verb 列の表示が異なります。
3.7 HTTP verbを制限する | Rails のルーティング - Railsガイド

 
例えば、

match '/multiple_match', to: 'multiple#call', via: [:get, :post]

と定義した場合、rails routes では HTTP Verb が GET|POST のように表示されます。

ちなみに、Railroadsプラグインの作成当初、このことに気づかず「Railways プラグインとルーティングの数が合わない...」と悩みました。

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/rails_routes_app

プルリクは以下の2つです。

IntelliJ Platform Plugin開発にて、KotestやBasePlatformTestCaseを使ったテストコードを書いてみた

先日 Railroads という、Rails開発向けのIntelliJ Platform Pluginを作りました。
RubyMine 2023.3系から、rails routes を便利に扱える Railways プラグインが動かなくなったので、代替プラグイン Railroads を作りました - メモ的な思考的な

 
最初に作った段階では「動くものを作り切る」ことを優先し、

  • 動作が正しいことは実機で担保
  • テストコードは後で追加

という方針でプラグインを作成・リリースしました。

ただ、今後の継続的なメンテナンスのことを考えると、テストコードがあると色々安心できそうです。

 
そこで、今回テストコードを追加してみたことから、メモを残します。

 
目次

 

環境

 

事前調査

Kotlin + IntelliJ Platform Pluginの環境でテストコードを書くのは初めてだったので、事前にいくつか調査しました。

 

IntelliJ Platform Pluginでテストコードを書くには

公式ドキュメントにはテストに関する記載があります。
Testing Overview | IntelliJ Platform Plugin SDK

これによると、IntelliJ Platform Plugin SDKでは

  • プラットフォームの機能のモック
  • UIテストをするための機能

などのテスト向けの機能が提供されているようでした。

 
また、IntelliJ Platformのテストは特定のフレームワークに依存せずに書くこともできるようです。

When writing your tests, you have the choice between using a standard base class to perform the test set up for you and using a fixture class, which lets you perform the setup manually and does not tie you to a specific test framework.

 

With the former approach, you can use classes such as BasePlatformTestCase (LightPlatformCodeInsightFixtureTestCase before 2019.2).

 

With the latter approach, you use the IdeaTestFixtureFactory class to create instances of fixtures for the test environment. You need to call the fixture creation and setup methods from the test setup method used by your test framework.

 
Tests and Fixtures | IntelliJ Platform Plugin SDK

 

Kotestのテストフレームワークについて

RailroadsプラグインはKotlinで書いていることから、テストフレームワークもKotlinのものを使いたいと考えました。

そこでKotlinのテストフレームワークを調べたところ、 Kotest がありました。
Kotest | Kotest

 
Kotestのドキュメントを見ると

など、いろいろ良さそうな機能がありました。

 
また、Kotlinのテストコードでモックを使いたい場合を調べたところ、 Mockk を使うのが良さそうでした。
MockK | mocking library for Kotlin

 

テストコードを書くときの方針について

ここまでのドキュメントより、 Kotest + Mockk + IntelliJ Platform Pluginの BasePlatformTestCaseIdeaTestFixtureFactory などを使えば、Kotlinでテストコードを書けそうでした。

ただ、 IdeaTestFixtureFactory の使い方は公式ドキュメントに記載されておらず、使いこなすには他のプラグインのテストコードを読んだりする必要がありそうでした。

 
そこで、現時点では「プラグインに対してテストコードを書く」ことを目的として、以下の方針でテストコードを書くことにしました。

  • テストを書く上で IntelliJ Platform Plugin SDKの機能が不要な部分は、Kotest で書く
  • テストを書く上で IntelliJ Platform Plugin SDKの機能が必要な部分は、 JUnit5 + BasePlatformTestCase で書く

 

Kotestでテストを書く

まずはKotestで書いてみます。

ただ、Railroadsプラグインでは IntelliJ Platform Plugin Template を使っていますが、そのテンプレートには Kotest の設定がありません。
https://github.com/JetBrains/intellij-platform-plugin-template

 
そこで今回は、Kotest を追加してから、Kotestでテストを書いていきます。

 

KotestやMockkを追加する

テストコードで KotestMockk を使えるようにするため、 build.gradle.ktsdependencies へ追加します。

dependencies {
    implementation(libs.annotations)
    val kotestVersion = "5.8.1"
    testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
    testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")

    val mockkVersion = "1.13.10"
    testImplementation("io.mockk:mockk:${mockkVersion}")
}

 

Kotest プラグインを追加する

Railroadsプラグインを書く IntelliJ IDEA Ultimateには、Kotestを容易に実行するための機能は提供されていません。

一方、Kotestでは IntelliJ IDEA向けのプラグインを提供しています。
IntelliJ Plugin | Kotest

そこで、開発用のIntelliJ IDEA UltimateにKotestプラグインを追加しておきます。
Kotest Plugin for JetBrains IDEs | JetBrains Marketplace

 

KotestのDescribe Specでテストコードを書く

公式ドキュメントや以下の記事にある通り、Kotestではテストコードのスタイルが複数用意されています。
Kotestの各種Specを比べてみた #Kotlin - Qiita

 
RailroadsはRails開発向けのプラグインであることから、Rails開発で見慣れている Describe Spec で書くことにします。

ちなみに、Describe Specは describecontextit キーワードを使って書けます。一方、検証は現在のRSpecのような expect記法ではなく、 shouldBe などを使うようです。

 
続いて、 IntelliJ Platform Plugin SDKの機能が不要な部分を探したところ、 models/routes ディレクトリのクラス群が該当しそうでした。

それらのクラスでは引数として module というIntelliJ Platformの機能を受け取っていますが、内部では使っていません。

 
そこで、

というのをKotestで実装したのが以下のテストコードです。

このテストコードを実行したところ、テストがパスしました。

package com.github.thinkami.railroads.models.routes

import com.github.thinkami.railroads.ui.RailroadIcon
import com.intellij.openapi.module.Module
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.mockk.mockk

class RedirectRouteTest: DescribeSpec({
    describe("RedirectRoute") {
        context ("set value at redirect route path") {
            // RedirectRoute does not use modules, so mock module
            val module = mockk<Module>()
            val actual = RedirectRoute(
                module,
                "GET",
                "/test",
                "redirect",
                "/test_redirect"
            )

            it("the title includes path") {
                actual.getActionIcon().shouldBe(RailroadIcon.NodeRedirect)
                actual.getActionTitle().shouldBe("/test_redirect")
                actual.getQualifiedActionTitle().shouldBe("redirect to /test_redirect")
            }
        }

        context ("set null at redirect route path") {
            // RedirectRoute does not use modules, so mock module
            val module = mockk<Module>()
            val actual = RedirectRoute(
                module,
                "GET",
                "/test",
                "redirect",
                null
            )

            it("the title is a fixed value") {
                actual.getActionIcon().shouldBe(RailroadIcon.NodeRedirect)
                actual.getActionTitle().shouldBe("[redirect]")
                actual.getQualifiedActionTitle().shouldBe("[runtime define redirect]")
            }
        }
    }
})

 

BasePlatformTestCaseでテストを書く

IntelliJ Platformの module などを使っている場合、 IdeaTestFixtureFactory を使えばテストコードは書けそうです。

ただ、前述の通り、 IdeaTestFixtureFactory に関する情報があまり得られないことから、現時点ではこの場合のテストコードをKotestで書くのが難しいと考えています。

 
そこで、この場合は、

  • BasePlatformTestCase を継承したテストクラスを使う
  • テストランナーとしてJUnit5を使う
    • KotestのテストランナーとしてJUnit5を使っているため、同じようにJUnit5を使いたい

という方針で進めます。

 

JUnit5を追加する

先ほどKotest向けのJUnit5の設定は追加しましたが、JUnit5向けの設定は追加していませんでした。

そこで、 build.gradle.ktsdependencies に対し、JUnit5を使うために必要な設定を追加します。

dependencies {
    // ...
    val junitVersion = "5.10.2"
    testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
    testRuntimeOnly("org.junit.vintage:junit-vintage-engine:${junitVersion}")
}

 

BasePlatformTestCaseクラスを継承してテストコードを書く

BasePlatformTestCase クラスを継承する例として、ここでは RailsRoutesParser の parse メソッドに対するテストコードを取り上げます。

parse メソッドでは rails routes の出力結果を、Railroadsで扱いやすいクラスの配列へと変換します。

そのため、テストでは配列の要素数が想定通りになっているかを検証すれば良さそうです。

 
ところで、JUnit5では rails routes を実行することができません。そこで

  • 事前に rails routes の実行結果をファイルへ保存する
  • テストコードではファイルを読み込む

という形にします。

今回は、以下の内容を src/test/testData/RailsRoutesParserTest.data.txt として保存します。

                                  Prefix Verb     URI Pattern                                                                                       Controller#Action
                          multiple_match GET|POST /multiple_match(.:format)                                                                         multiple#call
                      blog_post_comments GET      /blogs/:blog_id/posts/:post_id/comments(.:format)                                                 blogs/posts/comments#index
                                         POST     /blogs/:blog_id/posts/:post_id/comments(.:format)                                                 blogs/posts/comments#create
                   new_blog_post_comment GET      /blogs/:blog_id/posts/:post_id/comments/new(.:format)                                             blogs/posts/comments#new
                  edit_blog_post_comment GET      /blogs/:blog_id/posts/:post_id/comments/:id/edit(.:format)                                        blogs/posts/comments#edit
                       blog_post_comment GET      /blogs/:blog_id/posts/:post_id/comments/:id(.:format)                                             blogs/posts/comments#show
                                         PATCH    /blogs/:blog_id/posts/:post_id/comments/:id(.:format)                                             blogs/posts/comments#update
                                         PUT      /blogs/:blog_id/posts/:post_id/comments/:id(.:format)                                             blogs/posts/comments#update
                                         DELETE   /blogs/:blog_id/posts/:post_id/comments/:id(.:format)                                             blogs/posts/comments#destroy

 
続いてテストコードを書きます。

BasePlatformTestCase を使ったテストコードは

という形で実装したのが以下のテストコードです。

このテストコードを実行したところ、テストがパスしました。

package com.github.thinkami.railroads.parser

import com.intellij.testFramework.fixtures.BasePlatformTestCase
import junit.framework.TestCase
import java.io.File
import java.io.FileInputStream

class RailsRoutesParserParseTest: BasePlatformTestCase() {
    fun testParse() {
        val basePath = System.getProperty("user.dir")
        val inputStream = FileInputStream(File(basePath, "src/test/testData/RailsRoutesParserTest.data.txt"))
        val parser = RailsRoutesParser(module)
        val actual = parser.parse(inputStream)

        // 8 routes and 1 multiple route
        TestCase.assertEquals(10, actual.size)
    }
}

 

BasePlatformTestCase で ParameterizedTest を使う

ここでは RailsRoutesParser の parseLine メソッドに対するテストコードを取り上げます。

 
parseLine メソッドでは、 rails routes の出力結果をRailroadsで扱いやすいクラスへと変換します。

そのため、 rails routes の種類ごとにテストメソッドを用意すればよさそうです。ただ、この書き方だとテストメソッドが増えてしまい、メンテナンスが大変そうです。

 
そこで今回は、以下のJUnit5のドキュメントにある ParameterizedTest を使って検証とテストデータを分離します。
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests

 

ParameterizedTest を使うために必要なパッケージを追加する

build.gradle.ktsdependencies に対して追加します。

dependencies {
    // ...
    val junitVersion = "5.10.2"
    // ...
    // 追加
    testImplementation("org.junit.jupiter:junit-jupiter-params:${junitVersion}")
    // ...
}

 

ParameterizedTest を書く

ParameterizedTest を行うために以下を定義します。

  • テストメソッドに以下のアノテーションを追加する
    • ParameterizedTest アノテーション
    • ValueSource でテストデータを直接指定するか、テストデータが複雑であれば MethodSource を使ってメソッドの戻り値をテストデータにする
      • 今回はそこそこ複雑なので MethodSource を使う
  • MethodSource の場合、メソッドは以下のような形で作る
    • companion objectMethodSource で指定したメソッドを用意する
    • メソッドには JvmStatic アノテーションを付ける
    • 今回の場合、戻り値の型は Stream<Arguments> にする

 
また、BasePlatformTestCase は JUnit5対応されていないのか、JUnit5の ParameterizedTest と組み合わせて使おうとするといくつか問題が発生します。

そこで、以下の対応も追加で行います。
※ JUnit5や IntelliJ Platform のバージョンによっては発生しないかもしれません。

  • テストメソッドの中で setUp を呼ぶ
    • 背景
      • 何もしないと BasePlatformTestCasesetUp が呼ばれないようで、module などののIntelliJ Platform Plugin SDKで必要な値が BasePlatformTestCase に設定されない
      • そこで、ややトリッキーではあるがテストメソッドの中で setUp を呼び、 module などを設定する
  • ダミーのテストメソッドを定義する
    • 背景
      • ParameterizedTest だけを BasePlatformTestCase に定義した場合、テストメソッドがないと誤認されてしまう
      • そこで、常にパスするダミーのテストメソッドを定義することで、ParameterizedTest も認識してもらえるようにする

 
また、ここではRailwaysの以下のテストコードをベースにします。
https://github.com/basgren/railways/blob/master/test/net/bitpot/railways/parser/RailsRoutesParseLineTest.java

 
上記を踏まえて実装したのが以下のテストコードです。

このテストコードを実行したところ、テストがパスしました。

package com.github.thinkami.railroads.parser

import com.github.thinkami.railroads.models.routes.RedirectRoute
import com.github.thinkami.railroads.models.routes.SimpleRoute
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import junit.framework.TestCase
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import java.util.stream.Stream

class RailsRoutesParserParseLineTest: BasePlatformTestCase() {
    // The fact that BasePlatformTestCase predates JUnit5 may have an impact.
    //
    // If there is no test method and only JUnit5's parameterized test is used,
    // the test will fail with the following error.
    // junit.framework.AssertionFailedError: No tests found in com.github.thinkami.railroads.parser.RailsRoutesParserParseLineTest
    fun testDummy() {
        TestCase.assertEquals(1, 1)
    }

    @ParameterizedTest
    @MethodSource("variousRoute")
    fun testParseVariousLine(line: String, routeClass: String, routeName: String, method: String, path: String, actionTitle: String) {
        // The parameterized test is written in JUnit5, but the BasePlatformTestCase is implemented in a format earlier than JUnit5.
        // If nothing is done, the setUp method of BasePlatformTestCase is not executed and the module is not set in the fixture.
        // Therefore, by calling the setUp method, the module is set.
        setUp()

        val parser = RailsRoutesParser(module)
        val parsedLine = parser.parseLine(line)

        TestCase.assertNotNull(parsedLine)
        TestCase.assertEquals(1, parsedLine.size)

        val actual = parsedLine.first()
        TestCase.assertEquals(routeName, actual.routeName)
        TestCase.assertEquals(routeClass, actual::class.simpleName)
        TestCase.assertEquals(method, actual.requestMethod)
        TestCase.assertEquals(path, actual.routePath)
        TestCase.assertEquals(actionTitle, actual.getActionTitle())
    }

    companion object {
        @JvmStatic
        fun variousRoute(): Stream<Arguments> {
            return Stream.of(
                Arguments.arguments(
                    "    blog_post_comments GET      /blogs/:blog_id/posts/:post_id/comments(.:format)   blogs/posts/comments#index",
                    SimpleRoute::class.simpleName,
                    "blog_post_comments",
                    "GET",
                    "/blogs/:blog_id/posts/:post_id/comments(.:format)",
                    "blogs/posts/comments#index"),
                Arguments.arguments(
                    "  PUT      /blogs/:blog_id/posts/:post_id/comments/:id(.:format)   blogs/posts/comments#update",
                    SimpleRoute::class.simpleName,
                    "",
                    "PUT",
                    "/blogs/:blog_id/posts/:post_id/comments/:id(.:format)",
                    "blogs/posts/comments#update"),
                Arguments.arguments(
                    "  PATCH    /blogs/:blog_id/posts/:post_id/comments/:id(.:format)    blogs/posts/comments#update",
                    SimpleRoute::class.simpleName,
                    "",
                    "PATCH",
                    "/blogs/:blog_id/posts/:post_id/comments/:id(.:format)",
                    "blogs/posts/comments#update"),
                Arguments.arguments(
                    "   DELETE   /blogs/:blog_id/posts/:post_id/comments/:id(.:format)   blogs/posts/comments#destroy",
                    SimpleRoute::class.simpleName,
                    "",
                    "DELETE",
                    "/blogs/:blog_id/posts/:post_id/comments/:id(.:format)",
                    "blogs/posts/comments#destroy"),

                // route for Rack application
                Arguments.arguments(
                    "         rack_app    /rack_app(.:format)      #<HelloRackApp:0x000001988fad1b40>",
                    SimpleRoute::class.simpleName,
                    "rack_app",
                    "",
                    "/rack_app(.:format)",
                    "<HelloRackApp:0x000001988fad1b40>"),

                // inline handler
                Arguments.arguments(
                    "        inline GET      /inline(.:format)       Inline handler (Proc/Lambda)",
                    SimpleRoute::class.simpleName,
                    "inline",
                    "GET",
                    "/inline(.:format)",
                    ""),

                // route with additional requirements
                Arguments.arguments(
                    "  GET      /photos/:id(.:format)      photos#show {:id=>/[A-Z]\\d{5}/}",
                    SimpleRoute::class.simpleName,
                    "",
                    "GET",
                    "/photos/:id(.:format)",
                    "photos#show"),

                // redirect route
                Arguments.arguments(
                    "  redirect GET      /redirect(.:format)         redirect(301, /blogs)",
                    RedirectRoute::class.simpleName,
                    "redirect",
                    "GET",
                    "/redirect(.:format)",
                    "/blogs"),
            )
        }
    }
}

 

ソースコード

Railroadsのリポジトリにはテストコードを追加済です。
https://github.com/thinkAmi/railroads

テストコードを追加したときのプルリクはこちら。
https://github.com/thinkAmi/railroads/pull/10

ESLintプラグイン eslint-plugin-security はReactアプリの実装にも反応してくれるか試してみた

Reactアプリを実装するとき、LinterとしてESLintを使っています。

また、Vite.jsを使ってReactアプリを実装する場合、デフォルトで導入される

eslint-plugin-react などを使ってたりします。

 
そんな中、他にもReactのセキュリティ面を見てくれるESLintプラグインがないか調べたところ、Reactのセキュリティ関係のものについては

という状況でした。

 
一方、React以外のセキュリティについては eslint-plugin-security がありました。

リポジトリのREADMEには

ESLint rules for Node Security

This project will help identify potential security hotspots, but finds a lot of false positives which need triage by a human.

と書かれていましたが、Node.js以外にも使えそうな気がしたことから、ためしてみたときのメモを残します。

 
目次

 

環境

  • React 18.2.0
  • Vite 5.2.0
  • ESLint 8.57.0
  • eslint-plugin-security 3.0.0
  • ReDosに関する他のESLintプラグイン
    • eslint-plugin-regexp 2.5.0
    • eslint-plugin-redos 4.4.5

 

eslint-plugin-securityについて

READMEに記載のある通り、セキュリティに関するルールが定義されています。
https://github.com/eslint-community/eslint-plugin-security?tab=readme-ov-file#rules

Node.jsに関するものもありますが、以下の Regular Expression Denial of Service (ReDoS) のように Node.js環境以外でも使えそうなものも定義されたりします。
https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/regular-expression-dos-and-node.md

 
そこで今回は、ReDoSが発生するコードを実装した時、ESLintプラグイン eslint-plugin-security がルール違反を検知するか確認します。

 

Reactアプリの作成

まずは、ReDoSが発生するReactアプリを作成します。

 
最初に、Vite.jsでひな形となるReactアプリを生成します。

手順としてはVite.jsのドキュメントのままなので、ここでは記述を省略します。
https://vitejs.dev/guide/#scaffolding-your-first-vite-project

 
次に、メールアドレスに対してバリデーションが行うフォームを作成します。

そのフォームでは

  • eslint-plugin-security のドキュメントにある正規表現を使って、メールアドレスに対してバリデーションする
  • バリデーションの実行時間を計測する

を実装します。

import './App.css'

function App() {
  const handleSubmit = (e: React.SyntheticEvent) => {
    // https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forms_and_events/
    e.preventDefault()
    const startsAt = performance.now()
    const target = e.target as typeof e.target & {
      email: { value: string }
    }
    const email = target.email.value
    console.log(email)

    // ReDoS が発生するコード
    const emailExpression = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/
    if (emailExpression.test(email)) {
      console.log("OK")
    } else {
      console.log("NG")
    }
    console.log(performance.now() - startsAt)
  }

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input type="text" name={"email"} />
        <button type="submit">Run</button>
      </form>
    </>
  )
}

export default App

 
Reactアプリができたので、動作を確認します。

フォームの入力値を色々試したところ、文字数が増えるごとに処理時間が増えました。そのため、ReDoSが発生していると分かりました。

 

ESLintプラグイン eslint-plugin-security の挙動を確認する

ReDoSの発生が確認できたので、次は eslint-plugin-security の挙動を確認します。

まず、READMEに従い、 eslint-plugin-security をインストールします。
https://github.com/eslint-community/eslint-plugin-security

$ npm install --save-dev eslint-plugin-security

 
続いて、プラグインを設定します。

なお、Vite 5.2.0 の時点では .eslintrc.cjs が生成されるので、READMEの eslintrc config (deprecated) な書き方で設定します。

module.exports = {
  // ...
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
    'plugin:security/recommended-legacy',  // 追加
  ],
// ...

 
準備ができたのでWebStormでソースコードを開いたところ、該当箇所でESLintがルール違反を検知しました。

 
これより、 eslint-plugin-security はReactアプリであってもルール違反を検知すると分かりました。

 

余談:ReDoSに対する、ESLintプラグインについて

今回調べたReDoSについては、以下に詳しい記事がありました。
正規表現の脆弱性 (ReDoS) を JavaScript で学ぶ

また、他にもいくつかESLintプラグインがあったため、それらがどんな状況だったかをメモしておきます。

 

eslint-plugin-regexp

こちらは正規表現の誤りやスタイルガイドのESLintプラグインです。

こちらにもReDoSに関するルールはあります。
regexp/no-super-linear-backtracking | eslint-plugin-regexp

ただ、今回のコードの場合、ReDoSのルール以外で検知していました。

 
そこで、 eslint-plugin-regexp のドキュメントにあるコードを追加したところ、ルール違反を検知しました。

 

eslint-plugin-redos

以下の記事や資料を読んで、このプラグインを知りました。

 
インストールしてためしてみたところ、ESLintがルール違反を検知しました。

 
eslint-plugin-regexp 向けに追加したコードに対しても、ルール違反を検知しています。

 

ソースコード

Githubにあげました。
https://github.com/thinkAmi-sandbox/react_and_node_with_eslint-example

今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/react_and_node_with_eslint-example/pull/1

IntelliJ Platform Plugin開発にて、Plugin Signing を試してみた

IntelliJ Platform Plugin SDKのドキュメントを読んでいたところ、 Plugin Signing というページがありました。
Plugin Signing | IntelliJ Platform Plugin SDK

そこで、自作のプラグイン Railroads にPlugin Signingしてみたときのメモを残します。

 
目次

 

環境

 

署名の実施

ドキュメントに従い、署名を実施してみます。

 

秘密鍵の生成

ドキュメントの Generate Private Key に従い、秘密鍵を生成します。
https://plugins.jetbrains.com/docs/intellij/plugin-signing.html#generate-private-key

WSL2を開き、以下を実行します。

今回 pem ファイルは railroads_private_encrypted.pem とします。

また、パスワードも設定しておきます。

# 実行
$ openssl genpkey\
  -aes-256-cbc\
  -algorithm RSA\
  -out railroads_private_encrypted.pem\
  -pkeyopt rsa_keygen_bits:4096

# パスワード入力
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:

 
続いて、生成した鍵をRSA形式に変換します。

# 実行
$ openssl rsa\
  -in railroads_private_encrypted.pem\
  -out railroads_private.pem

# パスワードを求められるので、railroads_private_encrypted.pem を生成したときのパスワードを入力
Enter pass phrase for railroads_private_encrypted.pem:

# 結果
writing RSA key

 
最後に、 crt ファイルを生成します。

# 実行
$ openssl req\
  -key railroads_private.pem\
  -new\
  -x509\
  -days 3650\
  -out railroads_2024_0406.crt

# 注意書きが表示された
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.

# 適当な値を入力
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:thinkAmi
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:https://github.com/thinkAmi
Email Address []:

 

IDE環境変数に各鍵の値を設定

プラグインへの署名については

  1. Gradle IntelliJ Plugin の signPlugin タスク
  2. Gradle IntelliJ Plugin の publishPlugin タスク
  3. CLI

のいずれかの方法で行えば良いようです。

今回は

ということで、上記1. の方法で進めることにします。

 
次に、 signPlugin タスクで実行するためには、

  1. build.gradle.ktsファイルにて、秘密鍵などの情報をハードコード
  2. build.gradle.ktsファイルにて、秘密鍵などのファイルを読み込むよう指定
  3. IntelliJ IDEAの環境変数に、秘密鍵などをBASE64化した値を設定

のいずれかが必要そうでした。

今回は

ということで、上記3.の方法を取ることにしました。

 
そこで、IntelliJ IDEAの環境変数に設定できるよう、

  • railroads_2024_0406.crt
  • railroads_private.pem

の2ファイルの中身をBASE64化することにします。

また、都度BASE64化するのは手間なので、

  • certificate ディレクトリを作り、その中にpemやcrtを置く
  • 誤ってコミットしないよう、 .gitignore にて、 *.pem*.crt を追加する
  • certificate ディレクトリに何を置くか忘れないよう、pemやcrtのサンプルファイルを追加する
  • certificate ディレクトリの中にある pem や crt の中身をBASE64化するRubyスクリプトを作成する

という作業を行います。

 
ちなみに、Rubyスクリプト (base64encode.rb) は以下のような感じです。

なお、BASE64化するときに改行を追加しないよう、 Base64#strict_encode64 を使っています。
https://docs.ruby-lang.org/ja/latest/class/Base64.html#M_STRICT_ENCODE64

require 'base64'

file_paths = Dir.glob('*')
file_paths.each do |file_path|
  file_name = File.basename(file_path)

  # Do not output Base64-encoded strings for running Ruby scripts and example files
  next if file_name == File.basename(__FILE__)
  next if File.extname(file_name) == '.example'

  file_data = File.read(file_path)

  # Do not add newlines when Base64 encoded
  encoded_data = Base64.strict_encode64(file_data)

  puts "File Name: #{file_name}"
  puts "Encoded Data:"
  puts encoded_data
  puts "\n\n\n"
end

 
できたRubyスクリプトを実行し、BASE64化した値を確認します。

>ruby base64encode.rb
File Name: railroads_2024_0406.crt
Encoded Data:
***

File Name: railroads_private.pem
Encoded Data:
***

File Name: railroads_private_encrypted.pem
Encoded Data:
***

 
準備ができたので、公式ドキュメントに従い、IntelliJ IDEAで signPlugin タスクを追加します。
https://plugins.jetbrains.com/docs/intellij/plugin-signing.html#provide-secrets-to-ide

 
まず、 Run > Edit configuration から、 +Gradle を選択します。

続いて、以下の設定を行います。

  • Name: (任意の値)
  • Run on: Local machine (デフォルト)
  • Run: singPlugin
  • Gradle project: railroads (デフォルト)
  • Environment variables は、以下の3つの値を設定
    • なお、公式ドキュメントとは異なり、今回は署名するだけなので PUBLISH_TOKEN は設定不要
Name Value
CERTIFICATE_CHAIN railroads_2024_0406.crt の中身をBase64エンコードしたもの(改行なし)
PRIVATE_KEY railroads_private.pem の中身をBase64エンコードしたもの(改行なし)
PRIVATE_KEY_PASSWORD OpenSSLで railroads_private_encrypted.pem を作成したときに設定したパスワード

 
設定ができたので、 railroads [signPlugin] を実行します。

すると、以下のログが出力されました。成功したようです。

...
> Task :signPlugin

BUILD SUCCESSFUL in 59s
17 actionable tasks: 9 executed, 8 up-to-date
Configuration cache entry stored.
11:43:58: Execution finished 'signPlugin'.

 
また、 build/distributions/railroads-0.1.1-signed.zip というファイルも生成されていました。

 

署名の検証

では、次にプラグインの署名を検証します。

検証方法としては

  • Gradle で verifyPluginSignature タスクの実行
  • CLIで検証

の2つがありました。

Gradleのタスクで検証するためにドキュメントを読んだところ、環境変数から値を読み込んだり、動的に対象のプラグインファイルを指定するのが難しそうでした。
https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#tasks-verifypluginsignature

そこで今回はCLIで検証することにしました。

 
ドキュメントによると、CLIで検証するには CLI Tool が必要そうでした。
https://plugins.jetbrains.com/docs/intellij/plugin-signing.html#cli-tool

そこで、ドキュメントに従い、 marketplace-zip-signer-cli.jar をダウンロードしました。

 
続いて、署名をしたプラグインに対してツールを実行してみましたが、何も出力されませんでした。

> C:\Users\<UserName>\.jdks\jbr-17.0.9\bin\java.exe -jar marketplace-zip-signer-cli.jar verify -in "build\distributions\railroads-0.1.1-signed.zip" -cert "certificate\railroads_2024_0406.crt"

 
次に、署名がないプラグインに対してツールを実行したところ、メッセージ Provided zip archive is not signed が表示されました。

> C:\Users\<UserName>\.jdks\jbr-17.0.9\bin\java.exe -jar marketplace-zip-signer-cli.jar verify -in "build\distributions\railroads-0.1.1.zip" -cert "certificate\railroads_2024_0406.crt"

Provided zip archive is not signed

 
念のためソースコードを見たところ、成功したときは何も出力されないのが正解のようでした。
https://github.com/JetBrains/marketplace-zip-signer/blob/1b365366563540d8b70f46578cc10c3bcd541b13/cli/src/main/kotlin/org/jetbrains/zip/signer/ZipSigningTool.kt#L90

 

その他資料:Busy Plugin Developers

JetBrainsがYoutubeで公開している Busy Plugin Developers #1 の21分あたりから、Pluginについてふれられていました。
Plugin Signing at JetBrains Marketplace. IntelliJ Plugins UI Testing - YouTube

 

ソースコード

Githubに追加しています。
https://github.com/thinkAmi/railroads

今回のプルリクはこちら。
https://github.com/thinkAmi/railroads/pull/8

RubyMine 2023.3系から、rails routes を便利に扱える Railways プラグインが動かなくなったので、代替プラグイン Railroads を作りました

JetBrainsのIDE (IntelliJ IDEA Ultimate や RubyMine) で Rails アプリ開発をする場合、 Railways プラグインが手放せません。
Railways - RubyMine Plugin | Marketplace

 
上記のプラグインページにあるように、Railwaysには

  • rails routes の結果を一覧化する
    • パスとコントローラが紐づいていない場合は、 ? アイコンが表示される
  • 一覧から行を選択後、Actionに表示されているコントローラをクリックすると、そのコントローラが含まれるファイルが開く
  • コンテキストメニューから、パスや名前をコピーする

などの機能があります。

これらのおかげで、Railsアプリ開発を効率的に進めることができていました。

 
そんな中、RubyMineを 2023.3系にバージョンを上げたところ

Not compatible with the version of your running IDE (IntelliJ IDEA 2023.3.5, RubyMine 2023.3.5)

というメッセージが表示され、Railwaysプラグインがインストール・実行できなくなりました。

 
また、Githubリポジトリを見たところ、同じ現象に対する issue が立っていました。
Update for RubyMine Versions 2023.3.2+ · Issue #58 · basgren/railways

 
以前、Railwaysプラグインがロードされないというissueには回避策がありました。
Routes window not visible in IDEA 2022.1 · Issue #54 · basgren/railways

ただ、今回はそもそもインストール自体ができないことから、回避策がない可能性も考えられました。

 
このままでは色々つらいことから、Railways の代替となるプラグイン Railroads を作り、JetBrains Marketplaceで公開しました。
https://plugins.jetbrains.com/plugin/24076-railroads

 
Railways と同じような感覚で使えます。

 
そこで、この記事では

  • Railroads プラグインを作るまでの経緯
  • 現在のバージョン 0.1.0 でサポートしている機能

などについて書いていこうと思います。  
 
目次

 

対応の検討について

対応方法としては

  • forkして修正
  • イチから作る

のどちらかを考えました。

ここでは、なぜイチから作ることにしたかを記載します。

 

forkして修正しようとしたがビルドできなかった

イチから作るのは大変な気がしたので、まずは fork して修正する方向で考えました。

 
まずリポジトリのREADMEを読んだところ

  • Current release is tested on RubyMine 2016.1, RubyMine 2016.2, IntelliJ IDEA 2016.2.
  • Development Environment
  • Building the Plugin

などが記載されていました。

 
そこで、該当バージョンのIDEJDK、各種設定を行った上でビルドしてみたものの、ビルドできませんでした。

ビルドができないことには修正もできないことから、forkして修正する方向はあきらめました。

 

イチから作る

forkして修正ができないのであれば、イチから作るしかありません。

とはいえ、それはそれで考えることがありました。

 

何を使ってプラグインを作るか

昔からあるプラグインということもあり、Railwaysプラグイン

  • 開発言語は Java
  • ビルドは Ant

を使っているようでした。

 
一方、InttelliJ Platform Pluginのドキュメントを読んだところ、最近は

という構成でも開発できそうでした。

 
どちらで作るか少々悩みましたが、Kotlinをさわってみたかったということもあり、今回は Kotlin + Gradle + Template でプラグインを作ることにしました。

 

環境づくり

都合により、開発マシンは Windows + WSL2 です。

ふつうのRailsアプリ開発であれば、上記の開発マシンでもほとんど問題なく開発ができます。

一方、 IntelliJ Platform Plugin開発中に、プラグインの動作確認のため WSL2 上でIDEを起動しようとしましたが、JetBrains Gatewayを使っているとIDEが起動しませんでした。

 
これは mac もしくは Linux を用意するしかないのか...と思いましたが、

  • RubyInstaller for Windows を使って Ruby をインストール
  • その環境で Rails をインストール

という環境を作った上で、Windows上のRubyMineで Rails アプリを作ってみたところ、 rails routes が動作しました。

今回は rails routes さえ動けばプラグインの動作確認はできることから、いったんこれで良しとしました。

 
なお、動作確認に使った Rails アプリのリポジトリは以下です。
https://github.com/thinkAmi-sandbox/rails_routes_app

 

代替プラグインの名前について

Railways プラグインの機能を実現したいことから、名前も似た感じにしようと Railroads としました。

 

Railroads プラグインの機能について

次は、最初のリリースでは Railways プラグインのどの機能を移植するか考えました。

 

最初からサポートする機能

Railways プラグインのうちどの機能がよく使われているのか分かりませんでした。

そこでまずは自分が使いたい機能をサポートしようと考え、以下としました。

  • rails routes の結果を一覧化
  • ルートとコントローラが紐づいていない場合は、 ? アイコンが表示される
  • 一覧から行を選択後、Actionに表示されているコントローラをクリックすると、そのコントローラが含まれるファイルが開く
  • コンテキストメニューから、ルートのパスや名前をコピーできる

 
これらの機能については、Railways からそのまま移植できるものは移植し、難しいものについては Kotlin で書き直しています。

 

Railwaysとは別の形でサポートする機能

rails routes 時のエラーは Notification へ通知

Railwaysでは、 rails routes 時にエラーが発生した場合、エラーのダイアログにその内容を表示していました。

ただ、まずは動くものを作ろうと考えて、現時点ではエラーが発生したら IDE の Notification へ通知するようにしています。

 

アイコンは JetBrains のものを利用

Railwaysでは、png形式で各種アイコンを用意していました。
https://github.com/basgren/railways/tree/master/src/net/bitpot/railways/icons

一方、現在では、 png 形式ではなく svg 形式が推奨されています。
https://plugins.jetbrains.com/docs/intellij/icons.html#png-format-deprecated

ただ、時間の都合上、Railwaysで使っている png 形式のアイコンを svg 形式で用意するのは難しかったことから、今のところ JetBrains で用意しているアイコンで代替しています。

 

サポートを予定しない機能

rails routesの古いフォーマット

Railways のソースコードを読んだところ、 rails routes の昔のフォーマットをサポートしていました。

ただ、手元には昔のフォーマットで出力できる環境がなかったことから、昔のフォーマットのサポートはやめることにしました。

というのも、

あたりを読むとわかるのですが、Railways では

  • rails route を実行し、標準出力に結果を出力する
  • 標準出力から文字列を取り出し、正規表現でパース
  • パースした結果をいい感じに表示する

としてルーティングの一覧を作成しています。

そのため、 rails routes で昔のフォーマットを出力する環境がないのであれば、動作確認をするのが難しいと判断し、サポート対象外としました。

 
もし、昔のフォーマットでの出力に対応したい場合は、 RubyMine の古いバージョン + Railways プラグインを使ったほうが、機能は豊富だし適切だろうと考えています。

 

将来、必要に応じてサポートできればいいなと思う機能

Railways は機能豊富なため、それをはじめから全部移植するとプラグイン開発が終わりそうにもありませんでした。

そこで、最初のリリースでは自分が必要な機能だけに絞り実装しました。

 
そのため、以下のように「あると便利だけど、なくても何とかなるかも」という機能については、必要になった時点で実装・サポートする予定です。

 

ToolWindowを開いた時点で rails routes を実行する機能

Railways では実現できています。

ただ、 Railroads で実現するには「どのタイミングで rails routes をバックグラウンドで実行するのが良いか」がまだつかめていないことから、未実装となっています。

 

rails routes 結果のキャッシュ機能

こちらも Railways では実現できています。

ただ、

  • どのキャッシュ機構を使うのが適切か
  • キャッシュのリセットや追加はどのタイミングで行うのが適切か

など、まだ分からないことがあるため、実装していません。

 

environment やタスク名などのカスタマイズ機能

今のところはデフォルトの environment やタスク名でしか実行できません。

そのため、Railways同様、設定画面を用意して、カスタマイズされた環境でも実行できるようにできればいいなと考えています。

 

コードエディタでのナビゲーション機能

Railwaysにある

Adds quick navigation to action implementation from "Routes" panel or "Go to route action" popup available in code editor

という機能です。

自分の環境で使う機会がなかったので、まだ実装していません。

 

パスを絞り込んだときのハイライト機能

Railwaysでは、検索ボックスに入力することで、パスを絞り込めます。

Railroadsでも同様の機能を用意していますが、「入力値のどの部分がパスにマッチしているか」をハイライトするような機能はありません。

ハイライトするには、以下のディレクトリにあるような機能を実装しないといけないため、今は実装を先送りしています。
https://github.com/basgren/railways/tree/master/src/net/bitpot/railways/parser/route

 

テストコードの追加

IntelliJ Platform Plugin では、プラグインに対するテストコードも書くことができるようです。
Testing Overview | IntelliJ Platform Plugin SDK

 
Railsアプリ開発では、RSpecなどを使ってテストコードを書いています。

一方、 IntelliJ Platform Pluginに対するテストコードについては、今まで書いたことがありません。そのため、テストコードをきちんと書いて進めるとした場合、完成までに時間がかかりそうでした。

 
そこで、現時点では動作確認は実機で行うものとして、テストコードの実装は先送りしました。

なお、機能が増えるなどした場合、動作確認を実機で行うほうが手間がかかりそうと考えているため、そのうちテストコードを追加したいと思っています。

 

Railroads プラグインの公開URLについて

再掲となりますが、以下のページで公開しています。
https://plugins.jetbrains.com/plugin/24076-railroads

 

ソースコード

Railroads プラグインソースコードは、Githubで公開しています。
https://github.com/thinkAmi/railroads

まだまだ荒削りなプラグインですので、使ってみて何か気になることがありましたら、Githubのissueなどで情報を共有していただけるとありがたいです。

 
また、動作確認で使っている Rails アプリは以下で公開しています。
https://github.com/thinkAmi-sandbox/rails_routes_app

IntelliJ Platform Pluginの開発にて、Task.Backgroundableを使って重い処理をバックグラウンド実行してみた

以前、「ToolWindow上でボタンをクリックしたらラベルの値を更新する」をためしました。
IntelliJ Platform Pluginの開発にて、ApplicationManagerやToolWindowManagerを使って、Actionの中でToolWindow上のlabelを更新してみた - メモ的な思考的な

 
しかし、「ボタンをクリックした後、重い処理を実行し、その結果をもとにラベルを更新する」を行った場合、上記の方法だけではUIがフリーズしてしまいました。

そこで、UIのフリーズを避ける方法がないかを探したところ、 Task.Backgroundable を使えば良さそうでした。

ただ、残念ながら、 Task.Backgroundable については、IntelliJ Platform Pluginの公式ドキュメントに記載が見当たりませんでした (Background Tasks という項目は Will be available soon になっていたので、そのうち公開されるかもしれませんが)。

一方、JetBrainsの別のツール(MPS)にはそれっぽい使い方が記載されていました。
Running in the background | Progress indicators | MPS Documentation

 
そこでどんなふうに使えばよいか気になったことから、実際に Task.Backgroundable をためしてみたときのメモを残します。

 
目次

 

環境

 
なお、ソースコードは前回の記事の続きに追加していきます。

また、「ボタンをクリックした後、重い処理を実行し、その結果をもとにラベルを更新する」を確認するため、UIについては以前作成した UpdateInActionContent.kt に追加する形とします。

 

UIがフリーズしてしまう例

最初に、UIがフリーズしてしまう例として、「ボタンを押すと5秒ほど時間がかかった後にラベルを更新する」を実装します。

 

実装

まずはActionを用意します。この中で

  • 5秒待つ
  • ラベルを更新する

を実装します。

class HeavyActionWithoutBackground : AnAction() {
    override fun actionPerformed(e: AnActionEvent) {
        val project = e.getRequiredData(CommonDataKeys.PROJECT)

        // 5秒待つ
        Thread.sleep(5000)

        // ラベルを更新する
        ApplicationManager.getApplication().invokeLater {
            val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("UpdateInActionToolWindow")
                ?: return@invokeLater

            val panelComponent = toolWindow.component.components.filterIsInstance<DialogPanel>().first()
            val labelComponent = panelComponent.components.filterIsInstance<JLabel>().find {
                it.name == "myLabel"
            }

            if (labelComponent != null) {
                labelComponent.text = "Update By Heavy Action"
            }
        }
    }
}

 
続いて、ToolWindow上にボタンを用意します。

button("Heavy Action Without Background", HeavyActionWithoutBackground())

 

動作確認

では動作確認をします。

Heavy Action Without Background ボタンをクリックするとActionが実行されます。ただ、実行中はUI (ここでは右上の設定ボタン) を操作しようとしてもフリーズしています。

5秒経過後にラベルテキストが書き換わるとUIフリーズが解除され、設定メニューが表示されます。

 

Task.Backgroundableを使って、UIのフリーズを回避する例

続いて、 Task.Backgroundable を使った実装を見ていきます。

 

実装

まずは、 Task.Backgroundable を継承したクラスを用意します。

run メソッドをオーバーライドしてラベルの更新処理を実装します。

また、 run メソッドの引数 indicator を使い

indicator.fraction = 0.1
indicator.text = "Heavy process start"

と書くことで、バックグラウンドでの進捗を表現できます。上記の実装では、ステータスバーに10%の進捗と Heavy process start という文言が表示されます。

 
ソースコード全体はこちら。

class UpdateTask(private val project: Project): Task.Backgroundable(project, "task start...") {
    override fun run(indicator: ProgressIndicator) {
        indicator.fraction = 0.1
        indicator.text = "Heavy process start"

        Thread.sleep(5000)

        indicator.fraction = 0.9
        indicator.text = "Heavy process end"

        ApplicationManager.getApplication().invokeLater {
            val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("UpdateInActionToolWindow")
                ?: return@invokeLater

            val panelComponent = toolWindow.component.components.filterIsInstance<DialogPanel>().first()
            val labelComponent = panelComponent.components.filterIsInstance<JLabel>().find {
                it.name == "myLabel"
            }

            if (labelComponent != null) {
                labelComponent.text = "Update By Heavy Action With Background"
            }
        }
    }
}

 
続いて、Actionを実装します。

Task.Backgroundableインスタンスをキューに入れます。

class HeavyActionWithBackground : AnAction() {
    override fun actionPerformed(e: AnActionEvent) {
        val project = e.project ?: return
        UpdateTask(project).queue()
    }
}

 
あとはUIのボタンを実装すれば完成です。

button("Heavy Action With Background", HeavyActionWithBackground())

 

動作確認

では、実際に動かしてみます。

ボタンをクリックすると、ステータスバーに進捗が表示されました。また、UIはフリーズせず、設定ボタンのメニューも表示されます。

 

ソースコード

Githubにあげました。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin

今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin/pull/20

IntelliJ Platform Pluginの開発にて、開発環境にある公開したくない情報を local.properties に定義してビルドする

IntelliJ Platform Pluginの開発をする中で、ローカルマシンのファイルパスなど、開発環境にある公開したくない情報を定義したくなりました。

ただ、どのようにすればよいのか分からなかったため、調べたときのメモを残します。

 
目次

 

環境

 

調査

IntelliJ Platform Pluginのビルドでは Gradle を使っています。 Gradle IntelliJ Plugin | IntelliJ Platform Plugin SDK

ただ、IntelliJ Platform Pluginの公式ドキュメントでは、どのようにすればよいか記載が見当たりませんでした。

そのため、他にもGradleを使っているものがないかを調べたところ、AndroidのビルドでもGradleを使っていることが分かりました。

 
では、Androidではどのように設定しているかを調べたところ、多くの記事で local.properties を使っていました。

そこで、以下の記事を参考に、IntelliJ Platform Plugin開発でも local.properties を使ってみます。
android - Read value from local.properties via Kotlin DSL - Stack Overflow

 

実装

今回は runIdeideDir に対して、 local.properties で定義した値を設定してみます。

 

build.gradle.kts の修正

まずはファイルの先頭あたりで、 local.properties をロードします。

なお、 local.properties ファイルが無くてもエラーにならないよう、ファイルの存在チェックも行っています。

import java.io.FileInputStream
import java.util.*

// load local.properties
val localPropertiesFileExists = File(rootProject.rootDir, "local.properties").exists()
val prop = if (localPropertiesFileExists) Properties().apply {
    load(FileInputStream(File(rootProject.rootDir, "local.properties")))
} else null

 
続いて、runIde の設定を変更します。

ちなみに、 ideDir は設定する値に Property<File> な型を要求することから、 file() を使っています。

runIde {
    // executable other IDE
    if (prop != null) {
        val d = prop.getProperty("ideDir")
        if (d.isNotEmpty()) {
            // written by Kotlin
            ideDir.set(file(prop.getProperty("ideDir")))

            // written by Groovy
            // ideDir = file(file(prop.getProperty("ideDir")))
        }
    }

 

local.propertiesの作成

続いて、 local.properties ファイルを作成します。

今回定義する ideDir には、起動対象のIDEが存在するファイルパスを指定します。

今のところWindowsで開発しているため、 \エスケープした以下のような値を設定します。

ideDir = C:\\Users\\<UserName>\\AppData\\Local\\JetBrains\\Toolbox\\apps\\RubyMine\\<ch-n>\\232.10300.41

 
以上で設定は完了です。

 

動作確認

Run Plugin したところ、NotificationToolWindow を含む自作のプラグインがRubyMine 2023.2.6 で起動しました。

local.properties の値が読み込めているようです。

 

ソースコード

Githubにあげました。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin

今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin/pull/19

なお、今までのプルリクではGithub Actionsがエラーになっていましたが、今回のプルリクからは成功しています。