前言
各個團隊多少都有一些自己的代碼規(guī)范剩岳,但制定代碼規(guī)范簡單,困難的是如何落地入热。如果完全依賴人力Code Review
難免有所遺漏卢肃。
這個時候就需要通過靜態(tài)代碼檢查工具在每次提交代碼時自動檢查,本文主要介紹如何使用DeteKt
落地Kotlin
代碼規(guī)范才顿,主要包括以下內(nèi)容
- 為什么使用
DeteKt
? -
IDE
接入DeteKt
插件 -
CLI
命令行方式接入DeteKt
-
Gradle
方式接入DeteKt
- 自定義
Detekt
檢測規(guī)則 -
Github Action
集成Detekt
檢測
為什么使用DeteKt
?
說起靜態(tài)代碼檢查莫湘,大家首先想起來的可能是lint
,相比DeteKt
只支持Kotlin
代碼郑气,lint
不僅支持Kotlin
幅垮,Java
代碼,也支持資源文件規(guī)范檢查尾组,那么我們?yōu)槭裁床皇褂?code>Lint呢忙芒?
在我看來示弓,Lint
在使用上主要有兩個問題:
- 與
IDE
集成不夠好,自定義lint
規(guī)則的警告只有在運行./gradlew lint
后才會在IDE
上展示出來呵萨,在clean
之后又會消失 -
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)容:
-
DeteKt
開關(guān) - 格式化開關(guān),
DeteKt
直接使用了ktlint
的規(guī)則 -
Configuration file
:規(guī)則配置文件础米,可以在其中配置各種規(guī)則的開關(guān)與參數(shù)分苇,默認配置可見:default-detekt-config.yml -
Baseline file
:基線文件,跳過舊代碼問題屁桑,有了這個基線文件医寿,下次掃描時,就會繞過文件中列出的基線問題掏颊,而只提示新增問題。 -
Plugin jar
: 自定義規(guī)則jar
包艾帐,在自定義規(guī)則后打出jar
包乌叶,在掃描時就可以使用自定義規(guī)則了
DeteKt IDE
插件可以實時提示問題(包括自定義規(guī)則),如下圖所示柒爸,我們添加了自定義禁止使用kae
的規(guī)則:
對于一些支持自動修復的格式問題准浴,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
支持很多參數(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)可以完成,因此我們完成可以將DeteKt
與git 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.jar
與kotlin-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/**") // 過濾指定目錄
}
如上所示,接入主要需要做這么幾件事:
- 引入插件
- 配置插件藤肢,主要是配置
config
與baseline
太闺,即規(guī)則開關(guān)與老代碼過濾 - 引入
detekt-formatting
與自定義規(guī)則的依賴 - 配置
JvmTarget
,用于類型解析嘁圈,但不用再配置classpath
了省骂。 - 除了
baseline
之外,也可以通過include
與exclude
的方式指定只掃描指定文件的方式來實現(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"
)
)
}
}
}
代碼其實并不復雜牡整,主要做了這么幾件事:
- 添加
CustomRuleSetProvider
作為自定義規(guī)則的入口藐吮,并將NoSyntheticImportRule
添加進去 - 實現(xiàn)
NoSyntheticImportRule
類,主要包括issue
與各種visitXXX
方法 -
issue
屬性用于定義在控制臺或任何其他輸出格式上打印的ID
、嚴重性和提示信息 -
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