在团队开发中,使用共同的代码规范是必不可少的。但是制定代码规范简单,困难的是如何落地。如果完全依赖人力Code Review难免有所遗漏,这时就需要使用静态代码检查工具。常见的有 lint、detekt、Ktlint、checkstyle,它们之间的对比如下表所示:
| 功能 | lint | ktlint | detekt | checkstyle |
|---|---|---|---|---|
| 是否支持java和kotlin | 都支持 | 只支持kotlin | 只支持kotlin | 只支持Java |
| 是否支持检测资源文件 | 支持 | 不支持 | 不支持 | 不支持 |
| 是否支持自定义规则 | 支持 | 不支持 | 支持 | 不支持 |
| 是否存在 Idea 插件支持 | 不存在 | 不存在 | 存在 | 不存在 |
| 检测速度 | 检查速度可能较慢,尤其是在大型项目中 | Ktlint 是轻量级工具,检测速度快 | 检测速度较快 | 检测速度慢 |
可以看到,除了不支持检测资源文件外,detekt 相对于其他的静态代码检查工具在工具支持、检测速度、自定义规则上都有优势,因此这一篇文章将介绍如何使用 detekt 来做静态代码检查。
如何使用 Detekt
detekt 支持在 CLI(命令行)、gradle、idea插件使用,这里先介绍一下如何在 Gradle 中使用。
首先,我们需要先应用 detekt 的 gradle 插件,代码示例如下:
// 根目录的 build.gradle
plugins {
id 'com.android.application' version '8.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
id 'org.jetbrains.kotlin.jvm' version '1.9.22' apply false
id "io.gitlab.arturbosch.detekt" version "1.23.7" apply false // 引用 detekt 插件
}
// app 目录下的 build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id "io.gitlab.arturbosch.detekt" // 应用 detekt 插件
}
需要注意,需要代码检查的模块是一定要应用 io.gitlab.arturbosch.detekt 插件的,否则代码检查的时候会失败
应用插件完成后,就可以配置插件了。插件配置最重要的是规则配置文件,因为它决定了规则配置的各种规则的开关与参数。 代码示例如下,这里设置了 detekt-config.yml 规则配置文件。
// 配置插件
detekt {
// 规则配置
config.setFrom(files("${projectDir}/detekt-config.yml"))
}
在 detekt-config.yml 文件中,我们可以配置detekt自带的代码规则,也可以配置自定义的规则。这里用一个检查是否类为空的自带规则为例,代码示例如下:
empty-blocks:
active: true
EmptyClassBlock:
active: true
自带的所有规则配置可以在 default-detekt-config.yml中找到。而所有自带规则相关的文档在 Detekt Rule Set。常用的自定义规则有九类,它们的说明如下表所示:
| 规则大类 | 说明 |
|---|---|
| comments | 针对代码中的注释和文档进行规范检查,保障注释的合理性与文档的完整性,有助于提高代码可读性和可维护性。 |
| complexity | 检查代码的复杂程度,复杂度过高的代码往往会增加维护难度,该规则集有助于发现并优化这类代码。 |
| coroutines | 聚焦于与协程相关的代码规范检查,确保协程的使用符合最佳实践,避免潜在问题。 |
| empty-blocks | 主要检查代码中的空代码块,空代码块通常没有实际意义,应尽量避免,以此规则来规范代码编写。 |
| exceptions | 对代码中异常的抛出和捕获进行规范检查,保证异常处理的正确性和合理性,增强程序的稳定性。 |
| formatting | 处理代码的格式化问题,Detekt直接引用了ktlint的格式化规则集,使代码在格式上保持统一和规范。 |
| naming | 针对类名、变量命名等进行规范检查,统一命名风格,提升代码的可读性和可理解性。 |
| performance | 检测代码中潜在的性能问题,帮助开发者提前发现并优化可能影响程序性能的代码段。 |
| potential-bugs | 排查代码中潜在的BUG,降低程序运行时出现错误的风险,提高代码的可靠性。 |
| style | 用于统一团队的代码风格,除了一些基本的格式化问题外,还包括Detekt定义的特定风格规范,使团队代码风格保持一致。 |
设置好规则后,我们就可以创建一个空的类来测试 detekt 是否可以正常检测。代码示例如下:
class Test {
}
最后执行 ./gradlew detektDebug 就可以看到检测结果了。如下图所示:
有的时候,我们不想 detekt 检查之前出现的错误,那么可以执行 ./gradlew detektBaseline 生成 detekt-baseline.xml 文件,这时 detekt 就只会检测新出现的问题。
detekt 插件配置
detekt 插件的所有配置介绍如下所示,最新的可以见 Detekt Gradle Plugin | detekt
detekt {
// 将使用的 detekt 版本。未指定时,将使用找到的最新 detekt 版本。可覆盖此设置以使用相同版本。
toolVersion = "1.23.7"
// detekt 查找源文件的目录。
// 默认为 `files("src/main/java", "src/test/java", "src/main/kotlin", "src/test/kotlin")`。
source = files(
"src/main/kotlin",
"gensrc/main/kotlin"
)
// 并行构建抽象语法树。规则始终并行执行。
// 可加快大型项目的速度。默认为 `false`。
parallel = false
// 定义想要使用的 detekt 配置。
// 默认为默认的 detekt 配置。
config.setFrom("path/to/config.yml")
// 在 detekt 的默认配置文件基础上应用配置文件。默认为 `false`。
buildUponDefaultConfig = false
// 启用所有规则。默认为 `false`。
allRules = false
// 指定一个基准文件。后续运行 detekt 时,所有发现的问题都会存储在此文件中。
baseline = file("path/to/baseline.xml")
// 禁用所有默认的 detekt 规则集,仅使用通过 `detektPlugins` 配置传入的自定义规则运行 detekt。默认为 `false`。
disableDefaultRuleSets = false
// 在任务执行期间添加调试输出。默认为 `false`。
debug = false
// 如果设置为 `true`,当达到最大问题数量时,构建不会失败。默认为 `false`。
ignoreFailures = false
// Android:不为指定的构建类型(例如 "release")创建任务
ignoredBuildTypes = ["release"]
// Android:不为指定的构建变体(例如 "production")创建任务
ignoredFlavors = ["production"]
// Android:不为指定的构建变体(例如 "productionRelease")创建任务
ignoredVariants = ["productionRelease"]
// 为格式化报告中的文件路径指定基准路径。
// 如果未设置,报告的所有文件路径将是绝对文件路径。
basePath = projectDir
}
自定义规则
detekt 支持自定义规则。如果想要自定义规则,首先需要创建一个 Java or Kotlin Library 模块。如下图所示:
然后在该模块下的 build.gradle 中增加如下配置:
plugins {
id 'org.jetbrains.kotlin.jvm'
id "io.gitlab.arturbosch.detekt"
}
kotlin {
jvmToolchain(8)
}
dependencies {
implementation "io.gitlab.arturbosch.detekt:detekt-api:1.23.7"
testImplementation "io.gitlab.arturbosch.detekt:detekt-api:1.23.7"
testImplementation "io.gitlab.arturbosch.detekt:detekt-test:1.23.7"
}
在 Detekt 中,自定义规则的入口是 RuleSetProvider,具体规则的定义在 Rule 类中。代码示例如下,代码示例来源落地 Kotlin 代码规范,DeteKt 了解一下
// 自定义规则的入口
class CustomRuleSetProvider : RuleSetProvider {
// 规则大类的配置id
override val ruleSetId: String = "detekt-custom-rules"
override fun instance(config: Config): RuleSet = RuleSet(
ruleSetId,
listOf(
CustomRule(config),
)
)
}
// 具体的规则
class CustomRule(val config: Config) : Rule(config) {
override val issue = Issue(
"AvoidUseApiRule", // 规则配置id
Severity.Defect,
"Don’t use these function",
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"
)
)
}
}
}
}
编写好自定义规则后,最后需要设置入口的配置。如下图所示:
首先,我们需要创建 resources 目录,在该目录下分别创建 config/config.yml 和 META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider 文件。
config/config.yml 文件是我们自定义规则的配置,代码示例如下:
detekt-custom-rules:
active: true
AvoidUseApiRule:
active: true
META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider 文件则是设置入口的配置,示例如下:
com.example.rules.CustomRuleSetProvider
完成上述步骤后,就可以在 app 模块中使用自定义规则了。要使用自定义规则,首先需要在 app 模块下的 build.gradle 文件中增加依赖。
dependencies {
detektPlugins project(":rules")
}
然后在 detekt-config.yml 文件中增加自定义规则的配置,示例如下:
empty-blocks:
active: true
EmptyClassBlock:
active: true
detekt-custom-rules:
active: true
AvoidUseApiRule:
active: true
配置完成后,就可以创建一个类使用 Toast 来测试我们的自定义规则是否成功,代码示例如下:
class DetektTest(private val context: Context) {
fun test1() {
Toast.makeText(context, "测试toast", Toast.LENGTH_SHORT).show()
}
}
最后执行 ./gradlew detektDebug 就可以看到检测结果了。如下图所示:
detekt 的 api 文档为 detekt.dev/kdoc/detekt… ;detekt 中使用到了 KCP 的逻辑,对应的依赖为 org.jetbrains.kotlin:kotlin-compiler-embeddable,文档为:github.com/JetBrains/k…
在CLI中 使用
detekt 也支持命令行,一般用于流水线中。比如我们上传代码时,使用 git 的hook脚本执行 detekt 命令来检测代码是否符合规范。
要在 CLI 中使用 detekt,需要先在 detekt Releases 中下载 detekt-cli.jar 和 detekt-formatting.jar。如下图所示:
然后使用如下命令就可以了
java -jar detekt-cli-1.23.7-all.jar # detekt-cli.jar所在路径
-c F:\DetektDemo\app\detekt-config.yml # 规则配置文件所在路径
# detekt-formatting 是格式化规则jar,主要基于ktlint封装;rules 是我们自定义的规则jar,使用 , 连接
-p detekt-formatting-1.23.7.jar,F:\DetektDemo\rules\build\libs\rules.jar
# 需要扫描的源文件,多个路径之间用 , 连接
-i F:\DetektDemo\app\src\main\java\com\example\detektdemo\DetektTest.kt,F:\DetektDemo\app\src\main\java\com\example\detektdemo\Test.kt
效果如下图所示:
如果想要打包自定义规则为 jar 文件,可以执行
./gradlew jar命令,然后在 build/lib 中就可以找到对应的jar了;如果要在命令行生成 baseline,则需要使用java -jar detekt-cli-1.23.8-all.jar --input ./src/main/kotlin --baseline ./config/detekt/baseline.xml --create-baseline
使用 detekt Idea 插件
除了 Gradle 和 CLI 之外,Detekt 还提供了 Idea 的插件,方便我们实时检查。首先我们需要在 Plugins 中安装 detekt 插件。如下图所示:
然后在 setting -> tools -> detekt 中打开相应的配置面板
在配置面板中,我们需要配置规则配置和自定义规则,如下图所示:
然后,我们就可以在指定 kotlin 文件中,通过鼠标右键 -> Run detekt -> Analyze File 来进行代码检查了。如下图所示:
检查结果如下图所示:
需要注意,目前 detekt idea 插件不支持使用 bindingContext 获取语义,因此我们需要对规则进行如下的变更。否则,detekt idea 插件的代码检查将失效。
class CustomRule(val config: Config) : Rule(config) {
override val issue = Issue(
"AvoidUseApiRule", // 规则配置id
Severity.Defect,
"Don’t use these function",
Debt.TWENTY_MINS // 指解决该问题预期需要花费的时间
)
override fun visitReferenceExpression(expression: KtReferenceExpression) {
super.visitReferenceExpression(expression)
if (expression.text == "makeText") {
// detekt idea 插件不支持 bindingContext 获取语义
// val referenceDescriptor =
// bindingContext.get(BindingContext.REFERENCE_TARGET, expression)
// val packageName = referenceDescriptor?.containingPackage()?.asString()
// val className = referenceDescriptor?.containingDeclaration?.name?.asString()
report(
CodeSmell(
issue, Entity.from(expression), "禁止直接使用Toast,建议使用xxxUtils"
)
)
}
}
}
关于 bindingContext 的相关问题,可以看
Custom rules not showing errors/warning in Android studio IDE · Issue
和 Detekt custom rules that uses Type resolution doesn't work with IntelliJ plugin