落地 Kotlin 代碼規(guī)范连躏,DeteKt 了解一下~

前言

各個團隊多少都有一些自己的代碼規(guī)范剩岳,但制定代碼規(guī)范簡單,困難的是如何落地入热。如果完全依賴人力Code Review難免有所遺漏卢肃。

這個時候就需要通過靜態(tài)代碼檢查工具在每次提交代碼時自動檢查,本文主要介紹如何使用DeteKt落地Kotlin代碼規(guī)范才顿,主要包括以下內(nèi)容

  1. 為什么使用DeteKt?
  2. IDE接入DeteKt插件
  3. CLI命令行方式接入DeteKt
  4. Gradle方式接入DeteKt
  5. 自定義Detekt檢測規(guī)則
  6. Github Action集成Detekt檢測

為什么使用DeteKt?

說起靜態(tài)代碼檢查莫湘,大家首先想起來的可能是lint,相比DeteKt只支持Kotlin代碼郑气,lint不僅支持Kotlin幅垮,Java代碼,也支持資源文件規(guī)范檢查尾组,那么我們?yōu)槭裁床皇褂?code>Lint呢忙芒?

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

  1. IDE集成不夠好,自定義lint規(guī)則的警告只有在運行./gradlew lint后才會在IDE上展示出來呵萨,在clean之后又會消失
  2. lint檢查速度較慢奏属,尤其是大型項目,只對增量代碼進行檢查的邏輯需要自定義

DeteKt提供了IDE插件潮峦,開啟后可直接在IDE中查看警告囱皿,這樣可以在第一時間發(fā)現(xiàn)問題,避免后續(xù)檢查發(fā)現(xiàn)問題后再修改流程過長的問題

同時Detekt支持CLI命令行方式接入與Gradle方式接入忱嘹,支持只檢查新增代碼嘱腥,在檢查速度上比起lint也有一定的優(yōu)勢

IDE接入DeteKt插件

如果能在IDE中提示代碼中存在的問題,應該是最快發(fā)現(xiàn)問題的方式拘悦,DeteKt也貼心的為我們準備了插件齿兔,如下所示:

主要可以配置以下內(nèi)容:

  1. DeteKt開關(guān)
  2. 格式化開關(guān),DeteKt直接使用了ktlint的規(guī)則
  3. Configuration file:規(guī)則配置文件础米,可以在其中配置各種規(guī)則的開關(guān)與參數(shù)分苇,默認配置可見:default-detekt-config.yml
  4. Baseline file:基線文件,跳過舊代碼問題屁桑,有了這個基線文件医寿,下次掃描時,就會繞過文件中列出的基線問題掏颊,而只提示新增問題。
  5. Plugin jar: 自定義規(guī)則jar包艾帐,在自定義規(guī)則后打出jar包乌叶,在掃描時就可以使用自定義規(guī)則了

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

對于一些支持自動修復的格式問題准浴,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支持很多參數(shù),下面列出一些常用的条霜,其他可以參見:Run detekt using Command Line Interface

Usage: detekt [options]
  Options:
    --auto-correct, -ac
      支持自動格式化的規(guī)則自動格式化催什,默認為false
      Default: false
    --baseline, -b
      如果傳入了baseline文件,只有不在baseline文件中的問題才會掘出來
    --classpath, -cp
      實驗特性:傳入依賴的class路徑和jar的路徑宰睡,用于類型解析
    --config, -c
      規(guī)則配置文件蒲凶,可以配置規(guī)則開關(guān)及參數(shù)
    --create-baseline, -cb
      創(chuàng)建baseline气筋,默認false,如果開啟會創(chuàng)建出一個baseline文件旋圆,供后續(xù)使用
    --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
      自定義規(guī)則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 # 規(guī)則配置文件所在路徑
--plugins /path/to/detekt-formatting-1.21.0.jar # 格式化規(guī)則jar搀矫,主要基于ktlint封裝
-ac # 開啟自動格式化
-i $FilePath$ # 需要掃描的源文件,多個路徑之間用,或者;連接

通過如上方式進行代碼檢查速度是非澈⒌龋快的艾君,根據(jù)經(jīng)驗來說一般就是幾秒之內(nèi)可以完成,因此我們完成可以將DeteKtgit hook結(jié)合起來肄方,在每次提交commit的時候進行檢測冰垄,而如果是一些比較耗時的工具比如lint,應該是做不到這一點的

類型解析

上面我們提到了权她,DeteKt--classpth參數(shù)與--language-version參數(shù)虹茶,這些是用于類型解析的。

類型解析是DeteKt的一項功能隅要,它允許 Detekt 對您的 Kotlin 源代碼執(zhí)行更高級的靜態(tài)分析蝴罪。

通常,Detekt 在編譯期間無法訪問編譯器語義分析的結(jié)果步清,我們只能獲取Kotlin源代碼的抽象語法樹要门,卻無法知道語法樹上符號的語義,這限制了我們的檢查能力廓啊,比如我們無法判斷符號的類型欢搜,兩個符號究竟是不是同一個對象等

通過啟用類型解析,Detekt 可以獲取Kotlin編譯器語義分析的結(jié)果谴轮,這讓我們可以自定義一些更高級的檢查炒瘟。

而要獲取類型與語義,當然要傳入依賴的class第步,也就是classpath疮装,比如android項目中常常需要傳入android.jarkotlin-stdlib.jar

Gradle方式接入DeteKt

CLI方式檢測雖然快,但是需要手動傳入classpath粘都,比較麻煩廓推,尤其是有時候自定義規(guī)則需要解析我們自己的類而不是kotlin-stdlib.jar中的類時,那么就需要將項目中的代碼的編譯結(jié)果傳入作為classpath了翩隧,這樣就更麻煩了

DeteKt同樣支持Gradle插件方式接入受啥,這種方式不需要我們另外再配置classpath,我們可以將CLI命令行方式與Gradle方式結(jié)合起來,在本地通過CLI方式快速檢測滚局,在CI上通過Gradle插件進行完整的檢測

接入步驟

// 1\. 引入插件
plugins {
    id("io.gitlab.arturbosch.detekt").version("[version]")
}

repositories {
    mavenCentral()
}

// 2\. 配置插件
detekt {
    config = files("$projectDir/config/detekt.yml") // 規(guī)則配置
    baseline = file("$projectDir/config/baseline.xml") // baseline配置
    parallel = true
}

// 3\. 自定義規(guī)則
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用于創(chuàng)建Baseline
tasks.withType(DetektCreateBaselineTask).configureEach {
    jvmTarget = "1.8"
}

// 5\. 只分析指定文件
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
    // include("**/special/package/**") //  只分析 src/main/kotlin 下面的指定目錄文件
    exclude("**/special/package/internal/**") // 過濾指定目錄
}

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

  1. 引入插件
  2. 配置插件藤肢,主要是配置configbaseline太闺,即規(guī)則開關(guān)與老代碼過濾
  3. 引入detekt-formatting與自定義規(guī)則的依賴
  4. 配置JvmTarget,用于類型解析嘁圈,但不用再配置classpath了省骂。
  5. 除了baseline之外,也可以通過includeexclude的方式指定只掃描指定文件的方式來實現(xiàn)增量檢測

通過以上方式就接入成功了最住,運行./gradlew detektDebug就可以開始檢測了钞澳,掃描結(jié)果可在終端直接查看,并可以直接定位到問題代碼處涨缚,也可以在build/reprots/路徑下查看輸出的報告文件:

自定義Detekt檢測規(guī)則

要落地自己制定的代碼規(guī)范轧粟,不可避免的需要自定義規(guī)則,當然我們首先要看下DeteKt自帶的規(guī)則脓魏,是否已經(jīng)有我們需要的兰吟,只需把開關(guān)打開即可.

DeteKt自帶規(guī)則

DeteKt自帶的規(guī)則都可以通過開關(guān)配置,如果沒有在 Detekt 閉包中指定 config 屬性茂翔,detekt 會使用默認的規(guī)則混蔼。這些規(guī)則采用 yaml 文件描述,運行 ./gradlew detektGenerateConfig 會生成 config/detekt/detekt.yml 文件珊燎,我們可以在這個文件的基礎(chǔ)上制定代碼規(guī)范準則惭嚣。

detekt.yml 中的每條規(guī)則形如:

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

更多關(guān)于配置文件的修改方式悔政,請參考官方文檔-配置文件

Detekt 的規(guī)則集劃分為 9 個大類晚吞,每個大類下有具體的規(guī)則:

規(guī)則大類 說明
comments 與注釋、文檔有關(guān)的規(guī)范檢查
complexity 檢查代碼復雜度卓箫,復雜度過高的代碼不利于維護
coroutines 與協(xié)程有關(guān)的規(guī)范檢查
empty-blocks 空代碼塊檢查载矿,空代碼應該盡量避免
exceptions 與異常拋出和捕獲有關(guān)的規(guī)范檢查
formatting 格式化問題垄潮,detekt直接引用的 ktlint 的格式化規(guī)則集
naming 類名烹卒、變量命名相關(guān)的規(guī)范檢查
performance 檢查潛在的性能問題
potentail-bugs 檢查潛在的BUG
style 統(tǒng)一團隊的代碼風格,也包括一些由 Detekt 定義的格式化問題

更細節(jié)的規(guī)則說明弯洗,請參考:官方文檔-規(guī)則集說明

自定義規(guī)則

接下來我們自定義一個檢測KAE使用的規(guī)則旅急,如下所示:

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

// 自定義規(guī)則
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作為自定義規(guī)則的入口藐吮,并將NoSyntheticImportRule添加進去
  2. 實現(xiàn)NoSyntheticImportRule類,主要包括issue與各種visitXXX方法
  3. issue屬性用于定義在控制臺或任何其他輸出格式上打印的ID、嚴重性和提示信息
  4. visitImportDirective即通過訪問者模式訪問語法樹的回調(diào)谣辞,當訪問到import時會回調(diào)迫摔,我們在這里檢測有沒有添加kotlinx.android.synthetic,發(fā)現(xiàn)存在則報告異常

支持類型解析的自定義規(guī)則

上面的規(guī)則沒有用到類型解析泥从,也就是說不傳入classpath也能使用句占,我們現(xiàn)在來看一個需要使用類型解析的自定義規(guī)則

比如我們需要在項目中禁止直接使用android.widget.Toast.show,而是使用我們統(tǒng)一封裝的工具類躯嫉,那么我們可以自定義如下規(guī)則:

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回調(diào)中檢測表達式祈餐,我們不僅需要判斷是否存在Toast.makeTest表達式擂啥,因為可能存在同名類,更需要判斷Toast類的具體類型帆阳,而這就需要獲取語義信息

我們這里通過bindingContext來獲取表達式的語義哺壶,這里的bindingContext其實就是Kotlin編譯器存儲語義信息的表,詳細的可以參閱:K2 編譯器是什么舱痘?世界第二高峰又是哪座变骡?

當我們獲取了語義信息之后,就可以獲取Toast的具體類型芭逝,就可以判斷出這個Toast是不是android.widget.Toast塌碌,也就可以完成檢測了

Github Action集成Detekt檢測

在完成了DeteKt接入與自定義規(guī)則之后,接下來就是每次提交代碼時在CI上進行檢測了

一些大的開源項目每次提交PR都會進行一系列的檢測旬盯,我們也用Github Action來實現(xiàn)一個

我們在.github/workflows目錄添加如下代碼

name: Android CI

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

jobs:
  detekt-code-check:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: set up JDK 11
      uses: actions/setup-java@v3
      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的時候台妆,就都會自動調(diào)用該workflow進行檢測了,檢測不通過則不允許合并胖翰,如下所示:

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

總結(jié)

本文主要介紹了DeteKt的接入與如何自定義規(guī)則萨咳,通過IDE集成懊缺,CLI命令行方式與Gradle插件方式接入澈蝙,以及CI自動檢測昵观,可以保證代碼規(guī)范,IDE提示驶兜,CI檢測三者的統(tǒng)一舀凛,方便提前暴露問題俊扳,提高代碼質(zhì)量。

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

示例代碼

本文所有代碼可見:github.com/RicardoJian…

參考資料

detekt.dev/docs/intro
代碼質(zhì)量堪憂馋记?用 detekt 呀号坡,拿捏得死死的~

作者:程序員江同學
鏈接:https://juejin.cn/post/7152886037746827277

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市梯醒,隨后出現(xiàn)的幾起案子宽堆,更是在濱河造成了極大的恐慌,老刑警劉巖茸习,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件日麸,死亡現(xiàn)場離奇詭異,居然都是意外死亡逮光,警方通過查閱死者的電腦和手機代箭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涕刚,“玉大人嗡综,你說我怎么就攤上這事《拍” “怎么了极景?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長驾茴。 經(jīng)常有香客問我盼樟,道長,這世上最難降的妖魔是什么锈至? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任晨缴,我火速辦了婚禮,結(jié)果婚禮上峡捡,老公的妹妹穿的比我還像新娘击碗。我一直安慰自己,他們只是感情好们拙,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布稍途。 她就那樣靜靜地躺著,像睡著了一般砚婆。 火紅的嫁衣襯著肌膚如雪械拍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天装盯,我揣著相機與錄音坷虑,去河邊找鬼。 笑死验夯,一個胖子當著我的面吹牛猖吴,可吹牛的內(nèi)容都是我干的摔刁。 我是一名探鬼主播挥转,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绑谣?” 一聲冷哼從身側(cè)響起党窜,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎借宵,沒想到半個月后幌衣,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡壤玫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年豁护,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欲间。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡楚里,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出猎贴,到底是詐尸還是另有隱情班缎,我是刑警寧澤,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布她渴,位于F島的核電站达址,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏趁耗。R本人自食惡果不足惜沉唠,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望苛败。 院中可真熱鬧右冻,春花似錦、人聲如沸著拭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽儡遮。三九已至乳蛾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鄙币,已是汗流浹背肃叶。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留十嘿,地道東北人因惭。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像绩衷,于是被迫代替她去往敵國和親蹦魔。 傳聞我的和親對象是個殘疾皇子激率,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容