【OpenRewrite】コードを自動で一括変更

アプリケーションを運用していると、OSSのversion upや脆弱性対応等で複数のコード・アプリケーションに同じ変更を入れていかなければならないことがあります。

変更内容さえ決まってしまえば単純作業をするだけになってしまうのでできれば自動化したいですよね?

それ、OpenRewriteなら実現できます!

この記事では初めてOpenRewriteを使う方向けに導入方法から実際に自動で変更をする方法まで紹介します!

対象

  • Gradle

導入手順

まずはGradleアプリケーションにOpenRewriteを導入してみます。

plugins {
    ...
    // OpenRewriteのプラグインを追加
    // 最新versionはこちらで確認:https://plugins.gradle.org/plugin/org.openrewrite.rewrite
    id("org.openrewrite.rewrite") version("5.34.2")
}

...

rewrite {
    activeRecipe(
            // ここに書き換えのレシピを追加する
    )
}

これだけです!

実行方法

サンプルとして以下のJavaコードを対象に、
自動フォーマッティングをOpenRewriteで対応してみたいと思います。

public class SampleClass {
// 先頭にスペースを入れたい
private String message;
}

目的のレシピを探す

まずは、自動的にフォーマットしてくれるレシピを探します。

https://docs.openrewrite.org/ にアクセスし、検索窓でキーワード検索してみます。

今回はJavaのコードのフォーマットなのでJavaのFormatを選択します。

そうすると関連レシピのリストが表示されるので、
名前から目的を達成できそうなものを選択します。
※ひとつにしぼれなかったら、1つ1つ詳細を確認したり使ってみたりしましょう!

build.gradleを変更

レシピの詳細ページまでたどり着ければ、
具体的な使い方が書かれているのでその通りにbuild.graleを変更しましょう!

...

rewrite {
    activeRecipe("org.openrewrite.java.format.AutoFormat")
}

...

実行

レシピの設定が終わったらあとは実行するだけです!

OpenRewriteではDryrun(※)が用意されているので、事前に実行しておくとよいです!
(※)Dryrunは仮実行をして実行結果だけを確認するためのものです。

$ ./gradlew rewriteDryRun

実行結果は build/reports/rewrite/rewrite.patch に出力されています!

今回のコードだとこんな感じ

 public class SampleClass {
-private String message;
+    private String message;
 }

問題なければ本実行をしましょう!

$ ./gradlew rewriteRun

ここまでで、一通り実行ができるようになりました!

ここからは実用性のあることをもう少し紹介します!

レシピの管理

先ほどはactiveRecipeでレシピを設定するだけでレシピの登録ができました。

しかし、ものによってはrewrite.ymlが必要になるケースがあります。

例えばyamlファイルを更新できる以下のレシピです。

このレシピを利用しようとすると、まずプロジェクト直下にrewrite.ymlを用意し

type: specs.openrewrite.org/v1beta/recipe
name: sample.SampleRecipe
recipeList:
  - org.openrewrite.yaml.ChangeValue:
      oldKeyPath: $.path1.path2
      value: value2

nameで指定した識別子をactiveRecipeに設定します。

rewrite {
    activeRecipe("sample.SampleRecipe")
}

このようにrewrite.ymlが必要になるケースがあるのでそれだったら最初からrewrite.ymlでレシピを管理しておいた方が良いよねと思っています。

ちなみにレシピは以下のように複数設定することができます!

type: specs.openrewrite.org/v1beta/recipe
name: sample.SampleRecipe
recipeList:
  - org.openrewrite.yaml.ChangeValue:
      oldKeyPath: $.path1.path2
      value: value2
  - org.openrewrite.java.format.AutoFormat

独自レシピの開発

検索してもやりたいことを実現できるレシピが見つからない場合、独自にレシピを開発することも可能です!

ここでは特定のクラスにhelloメソッドを追加するレシピを自作してみようと思います。

準備

独自レシピはgradleプロジェクトでJavaで開発をします。

Gradleプロジェクトを作成した上で、build.gradleに変更を加えます。

plugins {
    ...
    // maven repositoryにアップロードするためのプラグイン
    id 'maven-publish'
}

dependencies {
    ...

    // Java関連のレシピ開発に必要
    implementation("org.openrewrite:rewrite-java:7.35.0")
    runtimeOnly("org.openrewrite:rewrite-java-8:7.35.0")
    runtimeOnly("org.openrewrite:rewrite-java-11:7.35.0")
    runtimeOnly('org.openrewrite:rewrite-java-17:7.35.0')

    // Maven関連のレシピ開発に必要
    // implementation("org.openrewrite:rewrite-maven:7.35.0")

    // Yaml関連のレシピ開発に必要
    // implementation("org.openrewrite:rewrite-yaml:7.35.0")

    // Properties関連のレシピ開発に必要
    // implementation("org.openrewrite:rewrite-properties:7.35.0")

    // XML関連のレシピ開発に必要
    // implementation("org.openrewrite:rewrite-xml:7.35.0")

    // レシピのテストに必要
    // testImplementation("org.openrewrite:rewrite-test:7.35.0")
}

// mavenリポジトリへのアップロードの設定
publishing {
    publications {
        mavenJava(MavenPublication) {
            groupId = 'sample'
            artifactId = 'openrewrite-sample'

            from components.java
        }
    }
}

開発

次にレシピの中身、書き換え内容の実装です!

package sample.recipe;

public class AddHelloMethod extends Recipe {
    // 表示名なので任意の文字列でOK
    @Override
    public String getDisplayName() {
        return "Add Hello Method";
    }

    // Visitorクラスを実装して返却する
    @Override
    protected JavaIsoVisitor<ExecutionContext> getVisitor() {
        return new AddHelloMethodVisitor();
    }

    // Visitorクラス(書き換えの具体内容を実装するクラス)
    public class AddHelloMethodVisitor extends JavaIsoVisitor<ExecutionContext> {

        @Override
        public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
            // 指定したクラス以外はスキップ
            if (!classDecl.getSimpleName().equals("SampleClass")) {
                return classDecl;
            }

            // helloメソッドの存在を確認
            boolean helloMethodExists = classDecl.getBody().getStatements().stream()
                    .filter(statement -> statement instanceof J.MethodDeclaration)
                    .map(J.MethodDeclaration.class::cast)
                    .anyMatch(methodDeclaration -> methodDeclaration.getName().getSimpleName().equals("hello"));
            // helloメソッドが存在する場合はスキップ
            if (helloMethodExists) {
                return classDecl;
            }

            // 挿入するコードを定義
            var helloTemplate = JavaTemplate.builder(this::getCursor, """
                        public String hello() {
                            return "Hello, OpenRewrite!!!";
                        }
                        """)
                    .build();

            // クラスの最後尾に定義したコードを挿入
            classDecl = classDecl.withBody(
                    classDecl.getBody().withTemplate(helloTemplate, classDecl.getBody().getCoordinates().lastStatement())
            );

            return classDecl;
        }
    }
}

(書き換えたい内容によって実装方法がいろいろありそうなので別記事でまとめたい、、、)

レシピの定義

次に作成したクラスをOpenRewriteが認識できるように設定をします。

設定はsrc/main/resources/META-INF/rewrite/rewrite.ymlで行います。

# 固定
type: specs.openrewrite.org/v1beta/recipe
# レシピの識別子。利用側の「recipeList」で指定する
name: sample.SampleRecipe
recipeList:
  # 定義したクラスの「パッケージ+クラス名」を指定する
  - sample.recipe.AddHelloMethod

Maven Repositoryへアップロード

最後にMaven Repositoryへアップロードしておしまいです!

$ ./gradlew publish

# 個人開発やお試し中はローカルリポジトリを使うのもあり
# $ ./gradlew publichToMavenLocal

利用側の設定


dependencies {
    ...
    // rewriteという独自の関数で依存を追加
    rewrite('sample:openrewrite-sample:+')
}
recipeList:
  # 独自レシピの「name」を指定
  - sample.SampleRecipe

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です