マルチモジュールプロジェクトにktlintのカスタムルールを入れる


意外と簡単にできるktlintのカスタムルール

## 専用モジュールを作成して依存を追加する

```kotlin
// build.gradle

plugins {
    kotlin("jvm")
}

/*
ktlint-engine-core = { module = "com.pinterest.ktlint:ktlint-rule-engine-core", version.ref = "ktlint" }
ktlint-cli-ruleset-core = { module = "com.pinterest.ktlint:ktlint-cli-ruleset-core", version.ref = "ktlint" }
ktlint-test = { module = "com.pinterest.ktlint:ktlint-test", version.ref = "ktlint" }
slf4j = { module = "org.slf4j:slf4j-simple", version = "2.0.12" }
 */
dependencies {
    implementation(libs.ktlint.engine.core)
    implementation(libs.ktlint.cli.ruleset.core)
    testImplementation(libs.junit)
    testImplementation(libs.ktlint.test)
    testRuntimeOnly(libs.slf4j)
}
```

- ktlint-engine-core

lintingとformattingを行う

- ktlint-cli-ruleset-core

標準のルールセットや、カスタムルールセットを定義するためのロジックが入ってる

- ktlint-test

UT用のパッケージ

- slf4j

ktlint自体がこのロギングライブラリのラッパーなので依存として必要（これなしだと、slf4j not found のエラーが出る）

[](https://github.com/pinterest/ktlint/blob/bcd538be182877bf4c5d9a0b387968e1c44016e0/documentation/snapshot/docs/api/custom-integration.md?plain=1#L113)

> Ktlint uses the `io.github.oshai:kotlin-logging` which is a `slf4j` wrapper. As API consumer you can choose which logging framework you want to use and configure that framework to your exact needs. The [basic API Consumer](https://github.com/pinterest/ktlint/blob/master/ktlint-api-consumer/src/main/kotlin/com/example/ktlint/api/consumer/KtlintApiConsumer.kt) contains an example with `org.slf4j:slf4j-simple` as logging provider and a customized configuration which shows logging at `DEBUG` level for all classes except one specific class which only displays logging at `WARN` level.
> 

https://pinterest.github.io/ktlint/latest/api/overview/

他にも部品はいっぱいあるが、最低限必要なのはこれだけ。

## カスタムルールの作成

```kotlin
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import giga.custom.ktlint.rules.CUSTOM_RULE_SET_ID
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

class NoPackageDeclaration : Rule(
    ruleId = RuleId("$CUSTOM_RULE_SET_ID:no-package-declaration"),
    about = About(), // Stack Traceに載せたい情報
) {
    
    override fun beforeVisitChildNodes(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
    ) {
        val hasNotPackageDirective = node.findChildByType(ElementType.PACKAGE_DIRECTIVE)?.getChildren(null)?.isEmpty()
        if (hasNotPackageDirective == true) {
            emit(0, "File does not have a package declaration", false)
        }
    }
}
```

今回はパッケージ宣言が空の場合にエラーにする処理を追加した。

- [ASTNode](https://github.com/JetBrains/intellij-community/blob/master/platform/core-api/src/com/intellij/lang/ASTNode.java)

抽象構文木のノード。この中にktファイルの構造が入っていて、これを交差して警告を出す形。

inspecter が提供されている

[Custom rule set - Ktlint](https://pinterest.github.io/ktlint/latest/api/custom-rule-set/#abstract-syntax-tree-ast)

## サービスプロバイダーの宣言

ktlintにカスタムルールを提供する

```kotlin
import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
import com.pinterest.ktlint.rule.engine.core.api.RuleSetId
import packagename.NoPackageDeclaration

internal val CUSTOM_RULE_SET_ID = "custom-rule-set-id"

class CustomRuleSetProvider : RuleSetProviderV3(RuleSetId(CUSTOM_RULE_SET_ID)) {
    override fun getRuleProviders(): Set<RuleProvider> =
        setOf(
            RuleProvider { NoPackageDeclaration() },
        )
}
```

```kotlin
// META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
packagename.CustomRuleSetProvider
```

ここまでのモジュールをビルドすると、build/libs/{module-name}.jar バイナリができる

## 生成したバイナリをktlintに登録

使っているビルドツールやライブラリによってお作法が変わるが、[kotlinter](https://github.com/jeremymailen/kotlinter-gradle?tab=readme-ov-file#custom-rules) だと ルートレベルのbuildScriptでバイナリを追加するだけ

```kotlin
buildscript {
    dependencies {
        classpath(files("path/to/{module-name}.jar "))
    }
}
```

## Test

カスタムルール専用のAsserterを使う

```kotlin
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule
import org.junit.Test

fun getRuleAsserter(provider: () -> Rule) = assertThatRule { provider() }

class NoPackageDeclarationTest {

    @Test
    fun test() {
        val wrappingRuleAssertThat = getRuleAsserter { NoPackageDeclaration() }
        val code =
            """
                
            class NoPackageDeclarationClassTest {}
            
            """.trimIndent()
        wrappingRuleAssertThat(code)
            .hasLintViolationWithoutAutoCorrect(1, 1, "File does not have a package declaration")
    }
}
```