跳转至

落地 Kotlin 代码规范,DeteKt 了解一下

前言

各个团队多少都有一些自己的代码规范,但制定代码规范简单,困难的是如何落地。如果完全依赖人力Code Review难免有所遗漏。

这个时候就需要通过静态代码检查工具在每次提交代码时自动检查,本文主要介绍如何使用DeteKt落地Kotlin代码规范,主要包括以下内容

  1. 为什么使用DeteKt?
  2. IDE接入DeteKt插件
  3. CLI命令行方式接入DeteKt
  4. Gradle方式接入DeteKt
  5. 自定义Detekt检测规则
  6. Github Action集成Detekt检测

为什么使用DeteKt?

说起静态代码检查,大家首先想起来的可能是lint,相比DeteKt只支持Kotlin代码,lint不仅支持KotlinJava代码,也支持资源文件规范检查,那么我们为什么不使用Lint呢?

在我看来,Lint在使用上主要有两个问题:

  1. IDE集成不够好,自定义lint规则的警告只有在运行./gradlew lint后才会在IDE上展示出来,在clean之后又会消失
  2. 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提交的文件

我们可以通过如下方式,下载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支持很多参数,下面列出一些常用的,其他可以参见: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$ # 需要扫描的源文件,多个路径之间用,或者;连接

通过如上方式进行代码检查速度是非常快的,根据经验来说一般就是几秒之内可以完成,因此我们完成可以将DeteKtgit hook结合起来,在每次提交commit的时候进行检测,而如果是一些比较耗时的工具比如lint,应该是做不到这一点的

类型解析

上面我们提到了,DeteKt--classpth参数与--language-version参数,这些是用于类型解析的。

类型解析是DeteKt的一项功能,它允许 Detekt 对您的 Kotlin 源代码执行更高级的静态分析。

通常,Detekt 在编译期间无法访问编译器语义分析的结果,我们只能获取Kotlin源代码的抽象语法树,却无法知道语法树上符号的语义,这限制了我们的检查能力,比如我们无法判断符号的类型,两个符号究竟是不是同一个对象等

通过启用类型解析,Detekt 可以获取Kotlin编译器语义分析的结果,这让我们可以自定义一些更高级的检查。

而要获取类型与语义,当然要传入依赖的class,也就是classpath,比如android项目中常常需要传入android.jarkotlin-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<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
    // include("**/special/package/**") //  只分析 src/main/kotlin 下面的指定目录文件
    exclude("**/special/package/internal/**") // 过滤指定目录
}

如上所示,接入主要需要做这么几件事:

  1. 引入插件
  2. 配置插件,主要是配置configbaseline,即规则开关与老代码过滤
  3. 引入detekt-formatting与自定义规则的依赖
  4. 配置JvmTarget,用于类型解析,但不用再配置classpath了。
  5. 除了baseline之外,也可以通过includeexclude的方式指定只扫描指定文件的方式来实现增量检测

通过以上方式就接入成功了,运行./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 定义的格式化问题

表格引用自:https://cloud.tencent.com/developer/article/1811025

更细节的规则说明,请参考:官方文档-规则集说明

自定义规则

接下来我们自定义一个检测KAE使用的规则,如下所示:

//  入口
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,而是使用我们统一封装的工具类,那么我们可以自定义如下规则:

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/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的时候,就都会自动调用该workflow进行检测了,检测不通过则不允许合并,如下所示:

点进去也可以看到详细的报错,具体是哪一行代码检测不通过,如图所示:

总结

本文主要介绍了DeteKt的接入与如何自定义规则,通过IDE集成,CLI命令行方式与Gradle插件方式接入,以及CI自动检测,可以保证代码规范,IDE提示,CI检测三者的统一,方便提前暴露问题,提高代码质量。

如果本文对你有所帮助,欢迎点赞~

示例代码

本文所有代码可见:https://github.com/RicardoJiang/android-workflow

参考资料

https://detekt.dev/docs/intro
代码质量堪忧?用 detekt 呀,拿捏得死死的~


最后更新: November 17, 2024