Kotlin - コードフォーマットにktlintを導入する

# ktlintとは

[ktlint - GitHub](https://github.com/pinterest/ktlint)

Kotlinのリンターで、フォーマッターも内蔵している

# モチベーション
開発チームでコードスタイルを統一する

# 便利に感じた点
- 設定ファイルを作成することなく公式コードスタイルを適応できる
- Gradleプラグインを公開している
- CIと統合できる

# Hands-on

## 動作環境
- windows 10
- Spring Boot 2.6.7
- Kotlin

### spring initializrでGradleプロジェクトを作成

[spring initializr](https://start.spring.io/)でGradleプロジェクトを作成する
[設定済みのリンク](https://start.spring.io/#!type=gradle-project&language=kotlin&platformVersion=2.6.7&packaging=jar&jvmVersion=11&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo)

### ktlintのGradleプラグインを追加
統合開発環境で開きbuild.gradle.ktsに[JLLeitschuh/ktlint-gradle](https://github.com/jlleitschuh/ktlint-gradle)を追加する

```diff_kotlin
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
	id("org.springframework.boot") version "2.6.7"
	id("io.spring.dependency-management") version "1.0.11.RELEASE"
+	id("org.jlleitschuh.gradle.ktlint") version "10.3.0"
	kotlin("jvm") version "1.6.21"
	kotlin("plugin.spring") version "1.6.21"
}
```

### コードスタイルの検証

```console
> gradlew ktlintCheck
```

or 

```console
> gradlew check
```

検証に失敗しているときコンソールに出力される
```console
C:\Users\Umizoko\Downloads\demo\demo>gradlew ktlintCheck

> Task :ktlintKotlinScriptCheck FAILED
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:4:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:5:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:6:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:7:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:8:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:16:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:20:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:21:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:22:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:23:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:27:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:28:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:29:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:30:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\build.gradle.kts:34:1 Unexpected tab character(s)

> Task :ktlintMainSourceSetCheck FAILED
C:\Users\Umizoko\Downloads\demo\demo\src\main\kotlin\com\example\demo\DemoApplication.kt:10:1 Unexpected tab character(s)

> Task :ktlintTestSourceSetCheck FAILED
C:\Users\Umizoko\Downloads\demo\demo\src\test\kotlin\com\example\demo\DemoApplicationTests.kt:9:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\src\test\kotlin\com\example\demo\DemoApplicationTests.kt:10:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\src\test\kotlin\com\example\demo\DemoApplicationTests.kt:11:1 Unexpected tab character(s)
C:\Users\Umizoko\Downloads\demo\demo\src\test\kotlin\com\example\demo\DemoApplicationTests.kt:12:1 Unexpected blank line(s) before "}"

FAILURE: Build completed with 3 failures.

...

BUILD FAILED in 1s
7 actionable tasks: 3 executed, 4 up-to-date

```

### 違反したコードを自動フォーマットする

```console
> gradlew ktlintCheck

BUILD SUCCESSFUL in 5s
7 actionable tasks: 6 executed, 1 up-to-date
```

### GitHub Actionsのワークフローでコードスタイルを検証し、違反がある場合は落とす

[Gradle Build Action](https://github.com/marketplace/actions/gradle-build-action)を利用してワークフローを設定する

```.github/workflows/check_workflow.yaml
name: Run Gradle on PRs
on: pull_request
jobs:
  gradle:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2

      - name: Change Permission
        run: chmod +x ./gradlew

      - name: Execute Gradle check
        run: ./gradlew check
```

### 違反したコードでPRを出してワークフローのチェックが落ちるか確認する

```console
> git branch illegal-code-style
> git switch illegal-code-style
```

コードスタイルを違反したファイルを追加

```src/main/kotlin/com/example/demo/domain/article/article
package com.example.demo.domain.article

class Article private constructor(
    val id: String,
    var title: String,
    val description: String
        ){

    fun changeTitle(title: String) {
        this.title = title
    }
}
```

```console
> git push --set-upstream origin illegal-code-style
```

GitHubからmasterブランチに対してPRを作成する

GitHubのワークフローがktlintの違反で落ちていることを確認

```console
Run ./gradlew check
Downloading https://services.gradle.org/distributions/gradle-7.4.1-bin.zip
...........10%...........20%...........30%...........40%...........50%...........60%...........70%...........80%...........90%...........100%

Welcome to Gradle 7.4.1!

...

> Task :ktlintMainSourceSetCheck FAILED
/home/runner/work/learn-ktlint/learn-ktlint/src/main/kotlin/com/example/demo/domain/article/Article.kt:1:1 File must end with a newline (\n)
/home/runner/work/learn-ktlint/learn-ktlint/src/main/kotlin/com/example/demo/domain/article/Article.kt:7:1 Unexpected indentation (8) (should be 0)
/home/runner/work/learn-ktlint/learn-ktlint/src/main/kotlin/com/example/demo/domain/article/Article.kt:7:9 Unexpected indentation (expected 0, actual 8)
/home/runner/work/learn-ktlint/learn-ktlint/src/main/kotlin/com/example/demo/domain/article/Article.kt:7:10 Missing spacing before "{"

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':ktlintMainSourceSetCheck'.
> A failure occurred while executing org.jlleitschuh.gradle.ktlint.worker.ConsoleReportWorkAction

   > KtLint found code style violations. Please see the following reports:
> Task :compileKotlin
6 actionable tasks: 6 executed
     - /home/runner/work/learn-ktlint/learn-ktlint/build/reports/ktlint/ktlintMainSourceSetCheck/ktlintMainSourceSetCheck.txt

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 1m 15s
Error: Process completed with exit code 1.
```

### 自動フォーマットを実行してリモートリポジトリを更新すると、ワークフローがパスする

```
Run ./gradlew check
To honour the JVM settings for this build a single-use Daemon process will be forked. See https://docs.gradle.org/7.4.1/userguide/gradle_daemon.html#sec:disabling_the_daemon.
Daemon will be stopped at the end of the build 
> Task :loadKtlintReporters
> Task :runKtlintCheckOverKotlinScripts
> Task :ktlintKotlinScriptCheck
> Task :runKtlintCheckOverMainSourceSet
> Task :ktlintMainSourceSetCheck
> Task :runKtlintCheckOverTestSourceSet
> Task :ktlintTestSourceSetCheck
> Task :processResources
> Task :processTestResources NO-SOURCE
> Task :compileKotlin
> Task :compileJava NO-SOURCE
> Task :classes
> Task :compileTestKotlin
> Task :compileTestJava NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test
> Task :check

BUILD SUCCESSFUL in 27s
11 actionable tasks: 11 executed
```

# まとめ
ktlintをwrapperしたプラグインを導入することで、ローカルで自動フォーマットを実行することができ、GitHub ActionsでPRをトリガーにリンターでの検証を行うことで、プロジェクトのコードスタイルを統一することができた。

使用したリポジトリ: https://github.com/Umizoko/learn-ktlint