当前位置:   article > 正文

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

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
  • 1
  • 2
  • 3

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路径,多个路径之间用,或者;连接
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在命令行可以直接通过如下方式检查

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$ # 需要扫描的源文件,多个路径之间用,或者;连接
  • 1
  • 2
  • 3
  • 4
  • 5

通过如上方式进行代码检查速度是非常快的,根据经验来说一般就是几秒之内可以完成,因此我们完成可以将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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

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

  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  # 有些规则,可以设定一个阈值
# ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

更多关于配置文件的修改方式,请参考官方文档-配置文件

Detekt 的规则集划分为 9 个大类,每个大类下有具体的规则:

规则大类说明
comments与注释、文档有关的规范检查
complexity检查代码复杂度,复杂度过高的代码不利于维护
coroutines与协程有关的规范检查
empty-blocks空代码块检查,空代码应该尽量避免
exceptions与异常抛出和捕获有关的规范检查
formatting格式化问题,detekt直接引用的 ktlint 的格式化规则集
naming类名、变量命名相关的规范检查
performance检查潜在的性能问题
potentail-bugs检查潜在的BUG
style统一团队的代码风格,也包括一些由 Detekt 定义的格式化问题

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

自定义规则

接下来我们自定义一个检测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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

代码其实并不复杂,主要做了这么几件事:

  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"
                    )
                )
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

可以看出,我们在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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

这样在每次提交PR的时候,就都会自动调用该workflow进行检测了,检测不通过则不允许合并,如下所示:

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

总结

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

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

示例代码

本文所有代码可见:github.com/RicardoJian…

参考资料

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

作者:程序员江同学
链接:https://juejin.cn/post/7152886037746827277

最后

这里就为大家分享一份谷歌技术团队开源的Kotlin全家桶学习资料(包含Kotlin 入门教程指南、高级Kotlin强化实战和史上最详Android版kotlin协程入门进阶实战 )如有需要完整版的可点击文末卡片查看获取方式!

《Kotlin入门教程指南》

第一章 Kotlin 入门教程指南

  • 前言

img

第二章 概述

  • 使用 Kotlin 进行服务器端开发
  • 使用 Kotlin 进行 Android 开发
  • Kotlin JavaScript 概述
  • Kotlin/Native 用于原生开发
  • 用于异步编程等场景的协程
  • Kotlin 1.1 的新特性
  • Kotlin 1.2 的新特性
  • Kotlin 1.3 的新特性

第三章 开始

  • 基本语法
  • 习惯用法
  • 编码规范

第四章 基础

  • 基本类型
  • 控制流:if、when、for、while
  • 返回和跳转

第五章 类与对象

  • 类与继承
  • 属性与字段
  • 接口
  • 可见性修饰符
  • 扩展
  • 数据类
  • 密封类
  • 泛型
  • 嵌套类与内部类
  • 枚举类
  • 对象表达式与对象声明
  • Inline classes
  • 委托

第六章 函数与 Lambda 表达式

  • 函数
  • 高阶函数与 lambda 表达式
  • 内联函数

第七章 其他

  • 解构声明
  • 集合:List、Set、Map
  • 区间
  • 类型的检查与转换“is”与“as”
  • This 表达式
  • 相等性
  • 操作符重载
  • 空安全
  • 异常
  • 注解
  • 反射
  • 类型安全的构建器
  • 类型别名
  • 多平台程序设计
  • 关键字与操作符

第八章 Java 互操作与 JavaScript

  • 在 Kotlin 中调用 Java 代码
  • Java 中调用 Kotlin
  • JavaScript 动态类型
  • Kotlin 中调用 JavaScript
  • JavaScript 中调用 Kotlin
  • JavaScript 模块
  • JavaScript 反射
  • JavaScript DCE

第九章 协程

  • 协程基础
  • 取消与超时
  • 通道 (实验性的)
  • 组合挂起函数
  • 协程上下文与调度器
  • 异常处理
  • select 表达式(实验性的)
  • 共享的可变状态与并发

第十章 工具

  • 编写 Kotlin 代码文档
  • Kotlin 注解处理
  • 使用 Gradle
  • 使用 Maven
  • 使用 Ant
  • Kotlin 与 OSGi
  • 编译器插件
  • 不同组件的稳定性

第十一章 常见问题总结

  • FAQ
  • 与 Java 语言比较
  • 与 Scala 比较【官方已删除】

高级Kotlin强化实战

第一章 Kotlin 入门教程

  • Kotlin 概述
  • Kotlin 与 Java 比较
  • 巧用 Android Studio
  • 认识 Kotlin 基本类型
  • 走进 Kotlin 的数组
  • 走进 Kotlin 的集合
  • 完整代码
  • 基础语法

第二章 Kotlin 实战避坑指南

  • 方法入参是常量,不可修改
  • 不要 Companion、INSTANCE?
  • Java 重载,在 Kotlin 中怎么巧妙过渡一下?
  • Kotlin 中的判空姿势
  • Kotlin 复写 Java 父类中的方法
  • Kotlin “狠”起来,连TODO都不放过!
  • is、as` 中的坑
  • Kotlin 中的 Property 的理解
  • also 关键字
  • takeIf 关键字
  • 单例模式的写法

第三章 项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始
  • Kotlin 写 Gradle 脚本是一种什么体验?
  • Kotlin 编程的三重境界
  • Kotlin 高阶函数
  • Kotlin 泛型
  • Kotlin 扩展
  • Kotlin 委托
  • 协程“不为人知”的调试技巧
  • 图解协程:suspend

欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证卡片免费领取↓↓↓

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/540392
推荐阅读
相关标签
  

闽ICP备14008679号