🔎

Ktlintのcustom ruleを実装する。

2024/04/04に公開

ktlint

Ktlintのcustom ruleの実装は以下のktlintやktlint-gradleのdocumentsの見れば分かるが、忘れたときにもう一度調べ直すのは面倒なのでまとめておく。

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を継承。
beforeVisitChildNodesafterVisitChildNodesを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/servicescom.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3というファイルを置いて、自前実装したRuleSetProviderV3を継承したクラスのFQCNを記述しておく。

com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
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