如何為Kotlin項(xiàng)目寫自定義Lint規(guī)則

原文發(fā)布于 Medium: Writing custom lint rules for your Kotlin project with detekt

相比于Java來講,Kotlin的代碼分析工具少得可憐。最近在GitHub上看到了一個(gè)叫detekt的項(xiàng)目,嘗試了一下,感覺十分好用。除了一般的代碼格式、復(fù)雜度檢查之外,它還可以做一些潛在bug、性能問題的檢查。它的README中已經(jīng)很好地講過了如何使用、配置默認(rèn)規(guī)則,這篇文章里我主要來詳細(xì)地講一下如何用它提供的接口寫自定義的規(guī)則。

把項(xiàng)目克隆到本地

自定義的規(guī)則需要依賴于detekt項(xiàng)目的detekt-api, detekt-core和detekt-test部分,而且我會(huì)用到項(xiàng)目中給的樣例來做講解,所以把項(xiàng)目克隆下來會(huì)方便一些。
git clone https://github.com/arturbosch/detekt.git

如何書寫規(guī)則

我們先來看看位于detekt/detekt-sample-ruleset中的TooManyFunctions規(guī)則:


/**
 * @author Artur Bosch
 * https://github.com/arturbosch/detekt/blob/master/detekt-sample-ruleset/src/main/kotlin/io/gitlab/arturbosch/detekt/sampleruleset/TooManyFunctions.kt
 */
class TooManyFunctions : Rule() {

    override val issue = Issue(javaClass.simpleName, Severity.CodeSmell, "")

    private var amount: Int = 0

    override fun visitFile(file: PsiFile) {
        super.visitFile(file)
        if (amount > 10) {
            report(CodeSmell(issue, Entity.from(file)))
        }
    }

    override fun visitNamedFunction(function: KtNamedFunction) {
        amount++
    }

}

detekt是基于Kotlin編譯器提供的抽象語法樹(AST)工作的,就是說你可以overridevisitFile()、visitClass()之類的函數(shù)。在一個(gè)visit函數(shù)中,調(diào)用super.visitXxx()會(huì)遍歷Xxx在AST中的所有子節(jié)點(diǎn)(當(dāng)然除非你override了一些子節(jié)點(diǎn)的visit方法而且沒有調(diào)用他們的super.visitXxx())。你也可以通過實(shí)現(xiàn)自己的DetektVisitor來做遍歷,舉個(gè)栗子,我們來看看detekt自帶的NestedBlockDepth規(guī)則:

/**
 * @author Artur Bosch
 * https://github.com/arturbosch/detekt/blob/master/detekt-rules/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/complexity/NestedBlockDepth.kt
 */
class NestedBlockDepth(config: Config = Config.empty, threshold: Int = 3) : ThresholdRule(config, threshold) {
    // ...
    override fun visitNamedFunction(function: KtNamedFunction) {
        val visitor = FunctionDepthVisitor(threshold)
        visitor.visitNamedFunction(function)
        if (visitor.isTooDeep)
            report(ThresholdedCodeSmell(issue, Entity.from(function), Metric("SIZE", visitor.maxDepth, threshold)))
    }

    private class FunctionDepthVisitor(val threshold: Int) : DetektVisitor() {
        internal var depth = 0
        internal var maxDepth = 0
        internal var isTooDeep = false

        private fun inc() {
            depth++
            if (depth > threshold) {
                isTooDeep = true
                if (depth > maxDepth) maxDepth = depth
            }
        }

        private fun dec() {
            depth--
        }

        override fun visitLoopExpression(loopExpression: KtLoopExpression) {
            inc()
            super.visitLoopExpression(loopExpression)
            dec()
        }
                // visit other blocks
    }
}

在這個(gè)規(guī)則中由于每個(gè)函數(shù)都要做自己的深度計(jì)數(shù),讓Visitor來保存計(jì)數(shù)會(huì)比像TooManyFunctions那樣用全局變量來計(jì)數(shù)簡(jiǎn)潔干凈得多。還有一個(gè)要注意的地方就是可以看到如果想讓你的規(guī)則可以接受自定義配置的話,在它的構(gòu)造函數(shù)里加上config: Config就可以了。

測(cè)試你的規(guī)則

Spek或者JUnit都可以測(cè)試規(guī)則。我們還是來看項(xiàng)目中給的例子:

/**
 * @author Artur Bosch
 * https://github.com/arturbosch/detekt/blob/master/detekt-sample-ruleset/src/test/kotlin/io/gitlab/arturbosch/detekt/sampleruleset/TooManyFunctionsSpec.kt 
**/
class TooManyFunctionsSpec : SubjectSpek<TooManyFunctions>({

    subject { TooManyFunctions() }

    describe("a simple test") {

        it("should find one file with too many functions") {
            val findings = subject.lint(code)
            assertThat(findings).hasSize(1)
        }
    }

})

class TooManyFunctionsTest : RuleTest {

    override val rule: Rule = TooManyFunctions()

    @Test fun findOneFile() {
        val findings = rule.lint(code)
        assertThat(findings).hasSize(1)
    }
}

val code: String =
  """
    你想測(cè)試的code放這里
  """

例子很簡(jiǎn)單清晰,就不多做說明了。這里只想強(qiáng)調(diào)兩點(diǎn):

  • 如果你選擇使用Spek,注意你要告訴父類SubjectSpeck還有下面的subject你在測(cè)試哪一條規(guī)則。
  • 在兩個(gè)測(cè)試中我們都能看到,subject/rule.lint(String)會(huì)編譯你給它的字符串然后用它來測(cè)試你的規(guī)則。如果你不想用字符串的方式來表達(dá)你的代碼的話,相對(duì)應(yīng)的還有subject/rule.lint(path: Path)函數(shù),只要把你的文件路徑傳進(jìn)去就可以了。還有一個(gè)比較有用的函數(shù)是Rule.format(String/Path), 顧名思義會(huì)把你傳進(jìn)去的代碼用detekt的格式規(guī)則整理好格式。

使用你的規(guī)則

cd detekt/detekt-sample-ruleset/
gradle build

你會(huì)看到detekt-sample-ruleset/build/libs文件夾里出現(xiàn)了兩個(gè)jar。 我們需要的是detekt-sample-ruleset-[版本號(hào)].jar。我們可以就在detekt這個(gè)項(xiàng)目中試用一下這些規(guī)則。打開detekt/build.gradle,在文件最底部可以看到一個(gè)大概長(zhǎng)這樣的detekt區(qū)塊:

detekt {
  // ...
  profile("main") {
    input = "$project.projectDir"
    filters = '.*/test/.*, .*/resources/.*, .*/build/.*'
    config = "$project.projectDir/detekt-cli/src/main/resources/default-detekt-config.yml"
    baseline = "$project.projectDir/reports/baseline.xml"
   }
 // ...
}

profile("main")那個(gè)區(qū)塊里加入一行ruleSets = “$projectDir/detekt-sample-ruleset/build/libs/detekt-sample-ruleset-[version].jar”就可以了。
現(xiàn)在在命令行運(yùn)行:

// 在detekt文件夾中
gradle detektCheck

就可以看到因?yàn)槲覀兊臉永?guī)則導(dǎo)致build failed:

Ruleset: sample
        TooManyFunctions - [Configurations.kt] at detekt-cli/src/main/kotlin/io/gitlab/arturbosch/detekt/cli/Configurations.kt:1:1

這樣就可以了,是不是很簡(jiǎn)單!


如果你想簡(jiǎn)歷你自己的規(guī)則集的話,有一些需要注意的地方:

  • 把你新建的模塊加入到detekt/settings.gradle里:
rootProject.name = 'detekt'
include 'detekt-api'
// ...
include 'detekt-migration'
include 'my-awesome-ruleset' //<--- 你的規(guī)則集在這
  • 每當(dāng)你新建一條規(guī)則的時(shí)候,都要把它加入到你的RuleSetProvider的規(guī)則集里:
class MyAwesomeProvider(override val ruleSetId: String = "awesome") : RuleSetProvider {
   override fun instance(config: Config): RuleSet {
      return RuleSet(ruleSetId, listOf(
            MyRule1(), // <--- 你的規(guī)則
            MyRule2()
      ))
   }
}
  • detekt用ServiceLoader來加載所有的規(guī)則,所以在你的模塊里一定要有一個(gè)文件resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider,在這個(gè)文件里要有你的RuleSetProvider的全名(比方說,io.gitlab.arturbosch.detekt.sampleruleset.SampleProvider)

差不多就是這樣了,希望有幫到你~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容