【初見者向け】Javaのリファクタリングツール OpenRewriteを少し検証

# はじめに
こんにちは。
少し前に、「OpenRewrite」というJavaのリファクタリングで使える？ツールの存在を知って
どのようなものか興味を持ったので、簡単に検証してみた記録です。

# OpenRewriteとは
### OpenRewriteの概要
>OpenRewriteは、Java言語に早期に焦点を当てて、フレームワークの移行、脆弱性のパッチ、およびAPIの移行のための大規模な分散ソースコードのリファクタリングを可能にします。

### OpenRewriteは何をしますか？
>OpenRewriteは、ソースコードを表す抽象構文木（AST）に変更を加え、変更されたツリーをソースコードに出力することで機能します。その後、コードの変更を確認してコミットできます。ASTの変更はビジターで実行され、ビジターはレシピに集約されます。OpenRewriteレシピは、元のフォーマットを尊重する最小限の侵襲的な変更をソースコードに加えます。

>たとえば、手動で行うのではなく、すべてのテストファイルで静的インポートを一貫して使用する場合は、OpenRewriteが提供するUseStaticImportビジターを使用できます。・・・（以下略）

（[公式サイト](https://docs.openrewrite.org/)より一部抜粋。Google日本語翻訳。）


フレームワークの移行だったり、手動だと大変なリファクタリング作業も自動で行なってくれたり、便利な機能が色々と使えそうです。
※余談ですが、Google日本語翻訳された文章をコピーしても、原文の英語がコピーされるのですね。。

# OpenRewriteで提供されているレシピ
公式チュートリアルでは以下のレシピが紹介されています。
（チュートリアルにあるのはほんの一部で、このほかにも数多くのレシピがあるので、興味ある方は公式を見てみてください。）

- Common Static Analysis Issue Remediation
- Automatically Fix Checkstyle Violations
- Migrate to Java 11 from Java 8
- Migrate to JUnit 5 from JUnit 4
- Migrate to Spring Boot 2 from Spring Boot 1
- Migrate to Quarkus 2 from Quarkus 1
- Migrate to Micronaut 3 from Micronaut 2
- Migrate to SLF4J from Log4j
- Use SLF4J Parameterized Logging
- Writing a Java Refactoring Recipe
- Modifying Methods with JavaTemplate
- Refactoring with Declarative YAML Recipes
- Automating Maven Dependency Management

# 検証
以下のレシピを検証してみました。順に見ていきたいと思います。
なお、今回試したコード全体は[こちらのGitHub](https://github.com/mytysoldier/openrewrite-sample)にコミット済みです。
検証コードは、SpringBoot + Gradleになります。

- Migrate to JUnit 5 from JUnit 4
- Migrate to Java 11 from Java 8

### [検証①] Migrate to JUnit 5 from JUnit 4
JUnit4で書かれたコードを、JUnit5に自動変換してくれます。

＜Step1＞ JUnit4のサンプルコードを用意する
テストコード

```java:SampleControllerTest.java
package com.example.demo.controller;

import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import com.example.demo.DemoApplication;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class)
public class SampleControllerTest {

    @Autowired
    private SampleController sampleController;

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(sampleController).build();
    }

    @Test
    public void sampleTestResponse() {
        try {
            // コントローラーリクエスト
            MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/sample"))
                    .andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
            assertEquals(result.getResponse().getContentAsString(), "sample");
        } catch (Exception ex) {
            ex.printStackTrace();
            fail("テスト失敗");
        }
    }

}
```

テスト対象コード

```java:SampleController.java
package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleController {

    @RequestMapping(method = RequestMethod.GET, path = "/sample")
    public String sample() {
        return "sample";
    }
    
}
```

＜Step2＞ OpenRewriteの構成を追加

```gradle:build.gradle
plugins {
	id 'org.springframework.boot' version '2.5.5'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id 'org.openrewrite.rewrite' version '5.10.0'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

rewrite {
	activeRecipe('org.openrewrite.java.spring.boot2.SpringBoot2JUnit4to5Migration')
	// activeRecipe('org.openrewrite.java.migrate.Java8toJava11')
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'junit:junit:4.12'
	rewrite("org.openrewrite.recipe:rewrite-spring:4.12.0")
	rewrite("org.openrewrite.recipe:rewrite-migrate-java:0.9.0")
}

test {
	useJUnitPlatform()
}
```

- 「id 'org.openrewrite.rewrite' version '5.10.0'」でOpenRewriteプラグインを追加
- 「activeRecipe('org.openrewrite.java.spring.boot2.SpringBoot2JUnit4to5Migration')」が今回使用するレシピ
- 「rewrite("org.openrewrite.recipe:rewrite-spring:4.12.0")」レシピに対応する依存関係を追加

＜Step3＞ OpenRewriteタスク実行
プロジェクト直下（build.gradleと同階層）にて、以下のコマンドでOpenRewriteタスクを実行することができます。

```
gradle rewriteRun
```

実行ログ（一部抜粋）。変更が加えられた旨、ログに出力されています。

```
Running recipe(s)...
Changes have been made to src/test/java/com/example/demo/controller/SampleControllerTest.java by:
    org.openrewrite.java.testing.junit5.AssertToAssertions
    org.openrewrite.java.testing.junit5.UpdateBeforeAfterAnnotations
    org.openrewrite.java.testing.junit5.UpdateTestAnnotation
    org.openrewrite.java.testing.junit5.RunnerToExtension
Please review and commit the results.
```

テストコード再掲（OpenRewrite実行後）

```java:SampleControllerTest.java
package com.example.demo.controller;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import com.example.demo.DemoApplication;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = DemoApplication.class)
public class SampleControllerTest {

    @Autowired
    private SampleController sampleController;

    private MockMvc mockMvc;

    @BeforeEach
    public void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(sampleController).build();
    }

    @Test
    public void sampleTestResponse() {
        try {
            // コントローラーリクエスト
            MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/sample"))
                    .andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
            assertEquals(result.getResponse().getContentAsString(), "sample");
        } catch (Exception ex) {
            ex.printStackTrace();
            fail("テスト失敗");
        }
    }

}
```

- importやアノテーションがJUnit5に置き換わっている。

#### 所感
- JUnit4→JUnit5のリファクタリングはコマンド一つで簡単にできるなという印象です

### [検証②] Migrate to Java 11 from Java 8
Java8の記法に沿って書かれたソースコードを、Java11記法で書き換えorコンパイルエラー警告してくれます。

＜Step1＞ JUnit8のサンプルコードを用意する
いくつかのパターン（①〜④）を試しました。

検証コード

```java:DemoApplication.java
package com.example.demo;

import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

	// java8→java11へリファクタリング検証用
	public static void executeSampleCode() {
		// パターン①：インスタンス化のリファクタリング
		Boolean bool = new Boolean(true);
		Byte b = new Byte("1");
		Character c = new Character('c');
		System.out.println(bool);
		System.out.println(b);
		System.out.println(c);

		// パターン②：「_」1文字は使用できなくなる
		String _ = "テスト太郎";
		System.out.println(_);

		// パターン③：暗黙的な型推論
		List<String> strList = new ArrayList<String>(){
			{
				add("test1");
				add("test2");
				add("test3");
			}
		};
		System.out.println(strList);
		
		// パターン④：削除されたメソッド
		new Thread().destroy();
	}

}
```

＜Step2＞ OpenRewriteの構成を追加（build.gradle再掲）

```gradle:build.gradle
plugins {
	id 'org.springframework.boot' version '2.5.5'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id 'org.openrewrite.rewrite' version '5.10.0'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

rewrite {
	// activeRecipe('org.openrewrite.java.spring.boot2.SpringBoot2JUnit4to5Migration')
	activeRecipe('org.openrewrite.java.migrate.Java8toJava11')
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'junit:junit:4.12'
	rewrite("org.openrewrite.recipe:rewrite-spring:4.12.0")
	rewrite("org.openrewrite.recipe:rewrite-migrate-java:0.9.0")
}

test {
	useJUnitPlatform()
}
```

- 「activeRecipe('org.openrewrite.java.migrate.Java8toJava11')」が今回使用するレシピ
- 「rewrite("org.openrewrite.recipe:rewrite-migrate-java:0.9.0")」レシピに対応する依存関係を追加

＜Step3＞ OpenRewriteタスク実行
同じくOpenRewriteタスクを実行。

```
gradle rewriteRun
```

#### パターン①：インスタンス化のリファクタリング

実行ログ（一部抜粋）。変更が加えられた旨、ログに出力されています。

```
Running recipe(s)...
Changes have been made to src/main/java/com/example/demo/DemoApplication.java by:
    org.openrewrite.java.cleanup.PrimitiveWrapperClassConstructorToValueOf
Please review and commit the results.
```

検証コード（抜粋再掲）

```java
// パターン①：インスタンス化のリファクタリング
Boolean bool = Boolean.valueOf(true);
Byte b = Byte.valueOf("1");
Character c = Character.valueOf('c');
System.out.println(bool);
System.out.println(b);
System.out.println(c);
```

変数宣言の記法が変更されています。
（例）「Boolean bool = new Boolean(true);」→「Boolean bool = Boolean.valueOf(true);」

#### パターン②：「_」1文字は使用できなくなる

実行ログ（一部抜粋）。警告は出力されたものの、ビルドは成功しました。

```
/openrewrite-sample/src/main/java/com/example/demo/DemoApplication.java:27: 警告:リリース9から'_'はキーワードなので識別子として使用することはできません
                String _ = "テスト太郎";
/openrewrite-sample/src/main/java/com/example/demo/DemoApplication.java:28: 警告:リリース9から'_'はキーワードなので識別子として使用することはできません
                System.out.println(_);
```

検証コード
OpenRewriteタスク実行で警告が出力されてはいるものの、ソースコードには変更ありませんでした。

#### パターン③：暗黙的な型推論

実行ログ（一部抜粋）。「Running recipe・・」のあとに「Changes have been made・・」のログが出力されていない。特に何も変わっていないということ？

```
Running recipe(s)...
```

検証コード（抜粋再掲）

```java
// パターン③：暗黙的な型推論
List<String> strList = new ArrayList<String>(){
	{
		add("test1");
		add("test2");
		add("test3");
	}
};
System.out.println(strList);
```

特に何も変わっていませんでした。。暗黙的型変換はOpenRewriteでは特に元のソースコードを書き換えたりといったことはしないようです。

#### パターン④：削除されたメソッド

実行ログ（一部抜粋）。ビルド失敗し、エラー内容がログに出力されています。

```
/openrewrite-sample/src/main/java/com/example/demo/DemoApplication.java:41: エラー: シンボルを見つけられません
                new Thread().destroy();
                            ^
  シンボル:   メソッド destroy()
  場所: クラス Thread
エラー1個

FAILURE: Build failed with an exception.
```

検証コード
OpenRewriteタスク実行でビルドエラーとなり、ソースコードには変更ありませんでした。

#### 所感

- Java8→Java11のリファクタリングに関しては、非推奨となっている記法やコンパイルエラーはビルド時に警告してくれるようです（OpenRewriteタスクを実行して、事前にどれだけのコンパイルエラーがあるのか全量を出したり、といった使い方ができるのかな。）
- パターン②はエラーにならないのですね。これもJava11では使えない記法だと思うので、ビルドエラーとしてほしいなと思いました
- まだまだ改良の余地は多そうですが、試してみる価値はあるかなといった印象です


# 参考情報
- [公式サイト](https://docs.openrewrite.org/)

# 終わりに
- OpenRewriteは公式で「Known Limitations」として現状では様々な制約があるものの、コマンド一つで一気にソースコードを自動変換してくれるのは個人的に良さげな印象でした
- 大規模なリファクタリングというと色々と骨の折れる作業のイメージですが、こういったツールも駆使して自動でできるところは積極的に使っていきたいですね
- 書き上げたコードはその時点から古くなっていく、後々のリファクタリングについて頭の片隅にでも置いておくことも大切だと改めて感じました
- 他のレシピも検証できたら、追記していきたいと思います