落地 Kotlin 程式碼規範,DeteKt 瞭解一下~

語言: CN / TW / HK

theme: smartblue highlight: a11y-dark


⚠️本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

前言

各個團隊多少都有一些自己的程式碼規範,但制定程式碼規範簡單,困難的是如何落地。如果完全依賴人力Code Review難免有所遺漏。

這個時候就需要通過靜態程式碼檢查工具在每次提交程式碼時自動檢查,本文主要介紹如何使用DeteKt落地Kotlin程式碼規範,主要包括以下內容

  1. 為什麼使用DeteKt?
  2. IDE接入DeteKt外掛
  3. CLI命令列方式接入DeteKt
  4. Gradle方式接入DeteKt
  5. 自定義Detekt檢測規則
  6. Github Action整合Detekt檢測

為什麼使用DeteKt?

說起靜態程式碼檢查,大家首先想起來的可能是lint,相比DeteKt只支援Kotlin程式碼,lint不僅支援KotlinJava程式碼,也支援資原始檔規範檢查,那麼我們為什麼不使用Lint呢?

在我看來,Lint在使用上主要有兩個問題:

  1. IDE整合不夠好,自定義lint規則的警告只有在執行./gradlew lint後才會在IDE上展示出來,在clean之後又會消失
  2. lint檢查速度較慢,尤其是大型專案,只對增量程式碼進行檢查的邏輯需要自定義

DeteKt提供了IDE外掛,開啟後可直接在IDE中檢視警告,這樣可以在第一時間發現問題,避免後續檢查發現問題後再修改流程過長的問題

同時Detekt支援CLI命令列方式接入與Gradle方式接入,支援只檢查新增程式碼,在檢查速度上比起lint也有一定的優勢

IDE接入DeteKt外掛

如果能在IDE中提示程式碼中存在的問題,應該是最快發現問題的方式,DeteKt也貼心的為我們準備了外掛,如下所示:

主要可以配置以下內容:
1. DeteKt開關 2. 格式化開關,DeteKt直接使用了ktlint的規則 3. Configuration file:規則配置檔案,可以在其中配置各種規則的開關與引數,預設配置可見:default-detekt-config.yml 4. Baseline file:基線檔案,跳過舊程式碼問題,有了這個基線檔案,下次掃描時,就會繞過檔案中列出的基線問題,而只提示新增問題。 5. Plugin jar: 自定義規則jar包,在自定義規則後打出jar包,在掃描時就可以使用自定義規則了

DeteKt IDE外掛可以實時提示問題(包括自定義規則),如下圖所示,我們添加了自定義禁止使用kae的規則:

對於一些支援自動修復的格式問題,DeteKt外掛支援自動格式化,同時也可以配置快捷鍵,一鍵自動格式化,如下所示:

CLI命令列方式接入DeteKt

DeteKt支援通過CLI命令列方式接入,支援只檢測幾個檔案,比如本次commit提交的檔案

我們可以通過如下方式,下載DeteKtjar然後使用

curl -sSLO https://github.com/detekt/detekt/releases/download/v1.22.0-RC1/detekt-cli-1.22.0-RC1.zip unzip detekt-cli-1.22.0-RC1.zip ./detekt-cli-1.22.0-RC1/bin/detekt-cli --help

DeteKt CLI支援很多引數,下面列出一些常用的,其他可以參見:Run detekt using Command Line Interface

Usage: detekt [options] Options: --auto-correct, -ac 支援自動格式化的規則自動格式化,預設為false Default: false --baseline, -b 如果傳入了baseline檔案,只有不在baseline檔案中的問題才會掘出來 --classpath, -cp 實驗特性:傳入依賴的class路徑和jar的路徑,用於型別解析 --config, -c 規則配置檔案,可以配置規則開關及引數 --create-baseline, -cb 建立baseline,預設false,如果開啟會創建出一個baseline檔案,供後續使用 --input, -i 輸入檔案路徑,多個路徑之間用逗號連線 --jvm-target EXPERIMENTAL: Target version of the generated JVM bytecode that was generated during compilation and is now being used for type resolution (1.6, 1.8, 9, 10, 11, 12, 13, 14, 15, 16 or 17) Default: 1.8 --language-version 為支援型別解析,需要傳入java版本 --plugins, -p 自定義規則jar路徑,多個路徑之間用,或者;連線

在命令列可以直接通過如下方式檢查

java -jar /path/to/detekt-cli-1.21.0-all.jar # detekt-cli-1.21.0-all.jar所在路徑 -c /path/to/detekt_1.21.0_format.yml # 規則配置檔案所在路徑 --plugins /path/to/detekt-formatting-1.21.0.jar # 格式化規則jar,主要基於ktlint封裝 -ac # 開啟自動格式化 -i $FilePath$ # 需要掃描的原始檔,多個路徑之間用,或者;連線

通過如上方式進行程式碼檢查速度是非常快的,根據經驗來說一般就是幾秒之內可以完成,因此我們完成可以將DeteKtgit hook結合起來,在每次提交commit的時候進行檢測,而如果是一些比較耗時的工具比如lint,應該是做不到這一點的

型別解析

上面我們提到了,DeteKt--classpth引數與--language-version引數,這些是用於型別解析的。

型別解析是DeteKt的一項功能,它允許 Detekt 對您的 Kotlin 原始碼執行更高階的靜態分析。

通常,Detekt 在編譯期間無法訪問編譯器語義分析的結果,我們只能獲取Kotlin原始碼的抽象語法樹,卻無法知道語法樹上符號的語義,這限制了我們的檢查能力,比如我們無法判斷符號的型別,兩個符號究竟是不是同一個物件等

通過啟用型別解析,Detekt 可以獲取Kotlin編譯器語義分析的結果,這讓我們可以自定義一些更高階的檢查。

而要獲取型別與語義,當然要傳入依賴的class,也就是classpath,比如android專案中常常需要傳入android.jarkotlin-stdlib.jar

Gradle方式接入DeteKt

CLI方式檢測雖然快,但是需要手動傳入classpath,比較麻煩,尤其是有時候自定義規則需要解析我們自己的類而不是kotlin-stdlib.jar中的類時,那麼就需要將專案中的程式碼的編譯結果傳入作為classpath了,這樣就更麻煩了

DeteKt同樣支援Gradle外掛方式接入,這種方式不需要我們另外再配置classpath,我們可以將CLI命令列方式與Gradle方式結合起來,在本地通過CLI方式快速檢測,在CI上通過Gradle外掛進行完整的檢測

接入步驟

``` // 1. 引入外掛 plugins { id("io.gitlab.arturbosch.detekt").version("[version]") }

repositories { mavenCentral() }

// 2. 配置外掛 detekt { config = files("$projectDir/config/detekt.yml") // 規則配置 baseline = file("$projectDir/config/baseline.xml") // baseline配置 parallel = true }

// 3. 自定義規則 dependencies { detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:1.21.0" detektPlugins project(":customRules") }

// 4. 配置 jvmTarget tasks.withType(Detekt).configureEach { jvmTarget = "1.8" } // DeteKt Task用於檢測,DetektCreateBaselineTask用於建立Baseline tasks.withType(DetektCreateBaselineTask).configureEach { jvmTarget = "1.8" }

// 5. 只分析指定檔案 tasks.withType().configureEach { // include("/special/package/") // 只分析 src/main/kotlin 下面的指定目錄檔案 exclude("/special/package/internal/") // 過濾指定目錄 }

```

如上所示,接入主要需要做這麼幾件事:

  1. 引入外掛
  2. 配置外掛,主要是配置configbaseline,即規則開關與老程式碼過濾
  3. 引入detekt-formatting與自定義規則的依賴
  4. 配置JvmTarget,用於型別解析,但不用再配置classpath了。
  5. 除了baseline之外,也可以通過includeexclude的方式指定只掃描指定檔案的方式來實現增量檢測

通過以上方式就接入成功了,執行./gradlew detektDebug就可以開始檢測了,掃描結果可在終端直接檢視,並可以直接定位到問題程式碼處,也可以在build/reprots/路徑下檢視輸出的報告檔案:

自定義Detekt檢測規則

要落地自己制定的程式碼規範,不可避免的需要自定義規則,當然我們首先要看下DeteKt自帶的規則,是否已經有我們需要的,只需把開關開啟即可.

DeteKt自帶規則

DeteKt自帶的規則都可以通過開關配置,如果沒有在 Detekt 閉包中指定 config 屬性,detekt 會使用預設的規則。這些規則採用 yaml 檔案描述,執行 ./gradlew detektGenerateConfig 會生成 config/detekt/detekt.yml 檔案,我們可以在這個檔案的基礎上制定程式碼規範準則。

detekt.yml 中的每條規則形如:

``` complexity: # 大類 active: true ComplexCondition: # 規則名 active: true # 是否啟用 threshold: 4 # 有些規則,可以設定一個閾值

...

```

更多關於配置檔案的修改方式,請參考官方文件-配置檔案

Detekt 的規則集劃分為 9 個大類,每個大類下有具體的規則:

規則大類 | 說明 | | -------------- | -------------------------------- | | comments | 與註釋、文件有關的規範檢查 | | complexity | 檢查程式碼複雜度,複雜度過高的程式碼不利於維護 | | coroutines | 與協程有關的規範檢查 | | empty-blocks | 空程式碼塊檢查,空程式碼應該儘量避免 | | exceptions | 與異常丟擲和捕獲有關的規範檢查 | | formatting | 格式化問題,detekt直接引用的 ktlint 的格式化規則集 | | naming | 類名、變數命名相關的規範檢查 | | performance | 檢查潛在的效能問題 | | potentail-bugs | 檢查潛在的BUG | | style | 統一團隊的程式碼風格,也包括一些由 Detekt 定義的格式化問題

更細節的規則說明,請參考:官方文件-規則集說明

自定義規則

接下來我們自定義一個檢測KAE使用的規則,如下所示:

```kotlin // 入口 class CustomRuleSetProvider : RuleSetProvider { override val ruleSetId: String = "detekt-custom-rules" override fun instance(config: Config): RuleSet = RuleSet( ruleSetId, listOf( NoSyntheticImportRule(), ) ) }

// 自定義規則 class NoSyntheticImportRule : Rule() { override val issue = Issue( "NoSyntheticImport", Severity.Maintainability, "Don’t import Kotlin Synthetics as it is already deprecated.", Debt.TWENTY_MINS )

override fun visitImportDirective(importDirective: KtImportDirective) {
    val import = importDirective.importPath?.pathStr
    if (import?.contains("kotlinx.android.synthetic") == true) {
        report(
            CodeSmell(
                issue,
                Entity.from(importDirective),
                "'$import' 不要使用kae,推薦使用viewbinding"
            )
        )
    }
}

} ```

程式碼其實並不複雜,主要做了這麼幾件事:
1. 新增CustomRuleSetProvider作為自定義規則的入口,並將NoSyntheticImportRule新增進去 2. 實現NoSyntheticImportRule類,主要包括issue與各種visitXXX方法 3. issue屬性用於定義在控制檯或任何其他輸出格式上列印的ID、嚴重性和提示資訊 4. visitImportDirective即通過訪問者模式訪問語法樹的回撥,當訪問到import時會回撥,我們在這裡檢測有沒有新增kotlinx.android.synthetic,發現存在則報告異常

支援型別解析的自定義規則

上面的規則沒有用到型別解析,也就是說不傳入classpath也能使用,我們現在來看一個需要使用型別解析的自定義規則

比如我們需要在專案中禁止直接使用android.widget.Toast.show,而是使用我們統一封裝的工具類,那麼我們可以自定義如下規則:

```kotlin class AvoidToUseToastRule : Rule() { override val issue = Issue( "AvoidUseToastRule", Severity.Maintainability, "Don’t use android.widget.Toast.show", Debt.TWENTY_MINS )

override fun visitReferenceExpression(expression: KtReferenceExpression) {
    super.visitReferenceExpression(expression)
    if (expression.text == "makeText") {
        // 通過bindingContext獲取語義
        val referenceDescriptor = bindingContext.get(BindingContext.REFERENCE_TARGET, expression)
        val packageName = referenceDescriptor?.containingPackage()?.asString()
        val className = referenceDescriptor?.containingDeclaration?.name?.asString()
        if (packageName == "android.widget" && className == "Toast") {
            report(
                CodeSmell(
                    issue, Entity.from(expression), "禁止直接使用Toast,建議使用xxxUtils"
                )
            )
        }
    }
}

} ```

可以看出,我們在visitReferenceExpression回撥中檢測表示式,我們不僅需要判斷是否存在Toast.makeTest表示式,因為可能存在同名類,更需要判斷Toast類的具體型別,而這就需要獲取語義資訊

我們這裡通過bindingContext來獲取表示式的語義,這裡的bindingContext其實就是Kotlin編譯器儲存語義資訊的表,詳細的可以參閱:K2 編譯器是什麼?世界第二高峰又是哪座?

當我們獲取了語義資訊之後,就可以獲取Toast的具體型別,就可以判斷出這個Toast是不是android.widget.Toast,也就可以完成檢測了

Github Action整合Detekt檢測

在完成了DeteKt接入與自定義規則之後,接下來就是每次提交程式碼時在CI上進行檢測了

一些大的開源專案每次提交PR都會進行一系列的檢測,我們也用Github Action來實現一個

我們在.github/workflows目錄新增如下程式碼

``` name: Android CI

on: push: branches: [ "main" ] pull_request: branches: [ "main" ]

jobs: detekt-code-check:

runs-on: ubuntu-latest

steps:
- uses: actions/[email protected]
- name: set up JDK 11
  uses: actions/[email protected]
  with:
    java-version: '11'
    distribution: 'temurin'
    cache: gradle

- name: Grant execute permission for gradlew
  run: chmod +x gradlew
- name: DeteKt Code Check
  run: ./gradlew detektDebug

```

這樣在每次提交PR的時候,就都會自動呼叫該workflow進行檢測了,檢測不通過則不允許合併,如下所示:

點進去也可以看到詳細的報錯,具體是哪一行程式碼檢測不通過,如圖所示:

總結

本文主要介紹了DeteKt的接入與如何自定義規則,通過IDE整合,CLI命令列方式與Gradle外掛方式接入,以及CI自動檢測,可以保證程式碼規範,IDE提示,CI檢測三者的統一,方便提前暴露問題,提高程式碼質量。

如果本文對你有所幫助,歡迎點贊~

示例程式碼

本文所有程式碼可見:https://github.com/RicardoJiang/android-workflow

參考資料

https://detekt.dev/docs/intro
程式碼質量堪憂?用 detekt 呀,拿捏得死死的~