如何為Kotlin項目寫自定義Lint規則

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

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

把項目克隆到本地

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

如何書寫規則

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


/**
 * @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()之類的函數。在一個visit函數中,調用super.visitXxx()會遍歷Xxx在AST中的所有子節點(當然除非你override了一些子節點的visit方法而且沒有調用他們的super.visitXxx())。你也可以通過實現自己的DetektVisitor來做遍歷,舉個栗子,我們來看看detekt自帶的NestedBlockDepth規則:

/**
 * @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
    }
}

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

測試你的規則

Spek或者JUnit都可以測試規則。我們還是來看項目中給的例子:

/**
 * @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 =
  """
    你想測試的code放這里
  """

例子很簡單清晰,就不多做說明了。這里只想強調兩點:

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

使用你的規則

cd detekt/detekt-sample-ruleset/
gradle build

你會看到detekt-sample-ruleset/build/libs文件夾里出現了兩個jar。 我們需要的是detekt-sample-ruleset-[版本號].jar。我們可以就在detekt這個項目中試用一下這些規則。打開detekt/build.gradle,在文件最底部可以看到一個大概長這樣的detekt區塊:

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")那個區塊里加入一行ruleSets = “$projectDir/detekt-sample-ruleset/build/libs/detekt-sample-ruleset-[version].jar”就可以了。
現在在命令行運行:

// 在detekt文件夾中
gradle detektCheck

就可以看到因為我們的樣例規則導致build failed:

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

這樣就可以了,是不是很簡單!


如果你想簡歷你自己的規則集的話,有一些需要注意的地方:

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容