意外と簡単にできるktlintのカスタムルール
専用モジュールを作成して依存を追加する
// 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 のエラーが出る)
Ktlint uses the
io.github.oshai:kotlin-logging
which is aslf4j
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 contains an example withorg.slf4j:slf4j-simple
as logging provider and a customized configuration which shows logging atDEBUG
level for all classes except one specific class which only displays logging atWARN
level.
他にも部品はいっぱいあるが、最低限必要なのはこれだけ。
カスタムルールの作成
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)
}
}
}
今回はパッケージ宣言が空の場合にエラーにする処理を追加した。
抽象構文木のノード。この中にktファイルの構造が入っていて、これを交差して警告を出す形。
inspecter が提供されている
サービスプロバイダーの宣言
ktlintにカスタムルールを提供する
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() },
)
}
// META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
packagename.CustomRuleSetProvider
ここまでのモジュールをビルドすると、build/libs/{module-name}.jar バイナリができる
生成したバイナリをktlintに登録
使っているビルドツールやライブラリによってお作法が変わるが、kotlinter だと ルートレベルのbuildScriptでバイナリを追加するだけ
buildscript {
dependencies {
classpath(files("path/to/{module-name}.jar "))
}
}
Test
カスタムルール専用のAsserterを使う
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")
}
}