一文了解 Detekt

947 阅读9分钟

在团队开发中,使用共同的代码规范是必不可少的。但是制定代码规范简单,困难的是如何落地。如果完全依赖人力Code Review难免有所遗漏,这时就需要使用静态代码检查工具。常见的有 lint、detekt、Ktlint、checkstyle,它们之间的对比如下表所示:

功能lintktlintdetektcheckstyle
是否支持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 就可以看到检测结果了。如下图所示:

屏幕截图 2025-01-31 205725.png

有的时候,我们不想 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 模块。如下图所示:

image.png

然后在该模块下的 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"
                    )
                )
            }
        }
    }
}

编写好自定义规则后,最后需要设置入口的配置。如下图所示:

image.png

首先,我们需要创建 resources 目录,在该目录下分别创建 config/config.ymlMETA-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 就可以看到检测结果了。如下图所示:

屏幕截图 2025-01-31 222812.png

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.jardetekt-formatting.jar。如下图所示:

image.png

然后使用如下命令就可以了

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

效果如下图所示:

image.png

如果想要打包自定义规则为 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 插件。如下图所示:

image.png

然后在 setting -> tools -> detekt 中打开相应的配置面板

image.png

在配置面板中,我们需要配置规则配置和自定义规则,如下图所示:

image.png

然后,我们就可以在指定 kotlin 文件中,通过鼠标右键 -> Run detekt -> Analyze File 来进行代码检查了。如下图所示:

image.png

检查结果如下图所示:

image.png

需要注意,目前 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 · IssueDetekt custom rules that uses Type resolution doesn't work with IntelliJ plugin

参考