落地 Kotlin 程式碼規範,DeteKt 瞭解一下~
theme: smartblue highlight: a11y-dark
⚠️本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!
前言
各個團隊多少都有一些自己的程式碼規範,但制定程式碼規範簡單,困難的是如何落地。如果完全依賴人力Code Review
難免有所遺漏。
這個時候就需要通過靜態程式碼檢查工具在每次提交程式碼時自動檢查,本文主要介紹如何使用DeteKt
落地Kotlin
程式碼規範,主要包括以下內容
- 為什麼使用
DeteKt
? IDE
接入DeteKt
外掛CLI
命令列方式接入DeteKt
Gradle
方式接入DeteKt
- 自定義
Detekt
檢測規則 Github Action
整合Detekt
檢測
為什麼使用DeteKt
?
說起靜態程式碼檢查,大家首先想起來的可能是lint
,相比DeteKt
只支援Kotlin
程式碼,lint
不僅支援Kotlin
,Java
程式碼,也支援資原始檔規範檢查,那麼我們為什麼不使用Lint
呢?
在我看來,Lint
在使用上主要有兩個問題:
- 與
IDE
整合不夠好,自定義lint
規則的警告只有在執行./gradlew lint
後才會在IDE
上展示出來,在clean
之後又會消失 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
提交的檔案
我們可以通過如下方式,下載DeteKt
的jar
然後使用
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$ # 需要掃描的原始檔,多個路徑之間用,或者;連線
通過如上方式進行程式碼檢查速度是非常快的,根據經驗來說一般就是幾秒之內可以完成,因此我們完成可以將DeteKt
與git hook
結合起來,在每次提交commit
的時候進行檢測,而如果是一些比較耗時的工具比如lint
,應該是做不到這一點的
型別解析
上面我們提到了,DeteKt
的--classpth
引數與--language-version
引數,這些是用於型別解析的。
型別解析是DeteKt
的一項功能,它允許 Detekt
對您的 Kotlin
原始碼執行更高階的靜態分析。
通常,Detekt
在編譯期間無法訪問編譯器語義分析的結果,我們只能獲取Kotlin
原始碼的抽象語法樹,卻無法知道語法樹上符號的語義,這限制了我們的檢查能力,比如我們無法判斷符號的型別,兩個符號究竟是不是同一個物件等
通過啟用型別解析,Detekt
可以獲取Kotlin
編譯器語義分析的結果,這讓我們可以自定義一些更高階的檢查。
而要獲取型別與語義,當然要傳入依賴的class
,也就是classpath
,比如android
專案中常常需要傳入android.jar
與kotlin-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
```
如上所示,接入主要需要做這麼幾件事:
- 引入外掛
- 配置外掛,主要是配置
config
與baseline
,即規則開關與老程式碼過濾 - 引入
detekt-formatting
與自定義規則的依賴 - 配置
JvmTarget
,用於型別解析,但不用再配置classpath
了。 - 除了
baseline
之外,也可以通過include
與exclude
的方式指定只掃描指定檔案的方式來實現增量檢測
通過以上方式就接入成功了,執行./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
參考資料
- kotlin-android-extensions 外掛到底是怎麼實現的?
- 江同學的 2022 年終總結,請查收~
- kotlin-android-extensions 外掛將被正式移除,如何無縫遷移?
- 學習一下 nowinandroid 的構建指令碼
- Kotlin 預設可見性為 public,是不是一個好的設計?
- 2022年編譯加速的8個實用技巧
- 落地 Kotlin 程式碼規範,DeteKt 瞭解一下~
- Gradle 進階(二):如何優化 Task 的效能?
- 開發一個支援跨平臺的 Kotlin 編譯器外掛
- 開發你的第一個 Kotlin 編譯器外掛
- Kotlin 增量編譯是怎麼實現的?
- Gradle 都做了哪些快取?
- K2 編譯器是什麼?世界第二高峰又是哪座?
- Android 效能優化之 R 檔案優化詳解
- Kotlin 快速編譯背後的黑科技,瞭解一下~
- 別了 KAPT , 使用 KSP 快速實現 ButterKnife
- Android Apk 編譯打包流程,瞭解一下~
- 如何優雅地擴充套件 AGP 外掛
- ASM 插樁採集方法入參,出參及耗時資訊
- Transform 被廢棄,ASM 如何適配?