Ktlintのcustom ruleを実装する。
Ktlintのcustom ruleの実装は以下のktlintやktlint-gradleのdocumentsの見れば分かるが、忘れたときにもう一度調べ直すのは面倒なのでまとめておく。
- User Guide | Custom rule set
- pinterest/ktlint ktlint-ruleset-templare
- JLLeitschuh/ktlint-gradle kotlin-rulesets-creating
- JLLeitschuh/ktlint-gradle kotlin-rulesets-using
Dependencies
implementation("com.pinterest.ktlint:ktlint-cli-ruleset-core:1.2.1")
testImplementation("com.pinterest.ktlint:ktlint-test:1.2.1")
testRuntimeOnly("org.slf4j:slf4j-simple:2.0.12")
※ versionは2024-04-04時点
ktlint-cli-ruleset-core
がRuleSetProviderやRuleを内包するmodule。
ktlint-test
が実装したRuleをテストするのに便利なmodule。
slf4j
はktlint-testが依存しているlogger。これがないとtestがNoClassDefFoundErrorで落ちる。
RuleSetProviderの実装
com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
を継承。
getRuleProviders
を実装する。
自前で実装したRuleをRuleProviderで包んでsetにまとめて返す。
RuleSetIdに使える文字列は[a-z]+(-[a-z]+)*
。
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
internal const val CUSTOM_RULE_SET_ID = "custom-rule-set-id"
class RuleSetProvider : RuleSetProviderV3(RuleSetId(CUSTOM_RULE_SET_ID)) {
override fun getRuleProviders(): Set<RuleProvider> {
return setOf(
RuleProvider { NoVarRule() }
)
}
}
Ruleの実装
com.pinterest.ktlint.rule.engine.core.api.Rule
を継承。
beforeVisitChildNodes
やafterVisitChildNodes
をoverrideして実装する。
引数としてASTNode
を貰えるのでAST(抽象構文木)をみてlintルールを実装していく。違反を見つけたらemitで違反箇所を指摘する。
ktlint-ruleset-standardの各ruleの実装は参考になる。
RuleIdに使える文字列は[a-z]+(-[a-z]+)*:[a-z]+(-[a-z]+)*
。:
をdelimiterとしてRuleSetIdと結合して定義する。
package io.moyuru.lintrules
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.psi.KtStringTemplateEntry
class NoVarRule : Rule(RuleId("$CUSTOM_RULE_SET_ID:novar"), About()) {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
if (node is LeafPsiElement && node.textMatches("var") &&
getNonStrictParentOfType(node, KtStringTemplateEntry::class.java) == null
) {
emit(node.startOffset, "Unexpected var, use val instead", false)
}
}
}
PsiViewer
以前はKtlintにASTを出力する機能が実装されていたが、もう無くなった。
PsiViewerというIntelliJのpluginが推奨されている。これで任意のktファイルのPsiを見て参考にするとルールを実装しやすい。
pluginはJetBrainsのsoftware engineerが個人名義で公開している。
Ruleをテストする
テストを実装すれば任意の文字列をlintしてくれるので、ruleの動作確認がしやすい。
assertThatRule
でRuleを包む。 高階関数になっていて、(String) -> KtLintAssertThat
を返してくれるのでこの関数に任意の文字列を渡せばlintしてくれる。
package io.moyuru.lintrules
import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule
import org.junit.Test
class NoVarRuleTest {
private val wrappingRuleAssertThat = assertThatRule { NoVarRule() }
@Test
fun `No var rule`() {
// whenever KTLINT_DEBUG env variable is set to "ast" or -DktlintDebug=ast is used
// com.pinterest.ktlint.test.(lint|format) will print AST (along with other debug info) to the stderr.
// this can be extremely helpful while writing and testing rules.
// uncomment the line below to take a quick look at it
// System.setProperty("ktlintDebug", "ast")
val code =
"""
fun fn() {
var v = "var"
}
""".trimIndent()
wrappingRuleAssertThat(code)
.hasLintViolationWithoutAutoCorrect(2, 5, "Unexpected var, use val instead")
}
}
META-INF
ktlintにRuleProviderとして認識させるためにメタ情報をいれておく必要がある。
src/main/resources/META-INF/services
にcom.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
というファイルを置いて、自前実装したRuleSetProviderV3を継承したクラスのFQCNを記述しておく。
io.moyuru.lintrules.RuleSetProvider
Custom Ruleを利用する
自分はktlint-gradleを使ってAndroidのprojectにktlintを組み込んでいるのでktlint-gradleでの用例。
jarに固めてから利用する方法もある(いろんなprojectで使い回せるので便利)が、自分の場合はproject内でサクッと実装して使えればそれでいいのでGradleのproject内にmoduleとして実装してAndroidのmoduleから使う。
Androidのbuild.gradleのdependenciesでktlintRuleset
でruleを実装したモジュールを指定する。
dependencies {
ktlintRuleset(project(":lint-rules"))
}
Discussion