Android静态代码扫描实践—4、自定义ktlint规则

1,927 阅读3分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前面3篇文章,我们介绍了静态代码扫描在团队的重要性以及在实际团队实践中如何使用Gitlab CI/CD配合静态代码扫描实现让团队成员低感知地遵守代码规范。而在之前我们的实践中仅仅是使用了 ktlint 实现了 Kotlind的官方代码风格规范 检查,但在实际开发过程中,我们还会有更多团队中的代码规范,如日志打印方法的统一、每个activity文件必须要有注释等。
因此,作为Android静态代码扫描实践的收官文章,我将带着大家如何使用 ktlint 写出自定义规则。

ktlint加载规则的流程

虽然官方也有简单的文档教我们如何自定义ktlint规则,但是我觉得先搞懂执行 ./gradlew ktlint 后如何加载规则,更有助于我们自定义ktlint规则。

首先先找到定义 ktlint 这个gradle任务的地方,在项目根目录下的app目录下的build.gradle里面

  ...
  configurations {
    ktlint
  }
  ...
  task ktlint(type: JavaExec, group: "verification") {
    description = "Check Kotlin code style."
    classpath = configurations.ktlint
    main = "com.pinterest.ktlint.Main"
    args "-a", "src/**/*.kt", "--reporter=html,output=${buildDir}/ktlint.html"
  }
  ...
  dependencies {
    ...
    ktlint("com.pinterest:ktlint:0.41.0") {
        attributes {
            attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL))
        }
    }
    ...
  }

其中定义了一个 name 为 ktlint 的 gradle 任务,类型为 JavaExec,执行后将会在子进程中执行 Java 应用程序(Jar),classpath 定义了要执行的 Jar 的路径,而 configurations.ktlint 是一个定义好的名为 ktlint 的引用集合,在这里面仅引用了 "com.pinterest:ktlint:0.41.0" ,后续你可以添加自己的Jar。 main 表示要执行的 main 方法为 com.pinterest.ktlint.Main.

因此我们可以直接在 ktlint 的源码看到 com.pinterest.ktlint.Main方法

main方法

我们执行 ./gradlew ktlint 的时候并没有带子命令,因此直接进入下一步 ktlintCommand.run() 方法。

ktlintCommand-run方法

其中 failOnOldRulesetProviderUsage() 是判断使用的 Jar 是否有继承老的规则方法,如果有,直接报错。而后续就是我们要找的加载规则的方法。

val ruleSetProviders = rulesets.loadRulesets(experimental, debug, disabledRules)

再进入看看loadRulesets方法

load-RuleSetProvider

可以看到加载了 Jar 里所有实现 RuleSetProvider 抽象类的类,当然还有一些过滤条件,而 RuleSetProvider 抽象类 get 方法返回了一系列实现 com.pinterest.ktlint.core.Rule 抽象类的规则类, 对后面的步骤还感兴趣的,大家可以去看ktlint的源码,这里我们只需要了解加载规则的流程。粗略总结如下图:

ktlint加载规则流程.png

因此我们自定义规则就是自定义一个类实现 RuleSetProvider 抽象类,在这个类中返回自定义的规则集合,然后导出成 Jar , 然后在项目根目录下的app目录下的build.gradle里面通过 ktlint 引用你的 Jar。

程序结构接口 (PSI)

上面简单介绍了 ktlin 如何加载自定义规则,了解后明白我们需要自定义一个类实现 RuleSetProvider 抽象类,在这个类中返回自定义的规则集合,而规则是一个实现 com.pinterest.ktlint.core.Rule 抽象类,在这个实现了规则的类中的 visit 抽象方法,在这个方法里面我们要完成识别不符合规范的代码块并输出警告提醒文本的功能,而该抽象方法的 ASTNode 参数就是我们识别代码块的关键。

ASTNode 是 JetBrains 对于旗下 IDE 的抽象语法树(Abstract Syntax Tree,AST)的实现 -- PSI(程序结构接口)其中的一个类。以树状的形式表现编程语言,将我们程序员所编写的源代码语法结构进行抽象表示。可以理解为 PSI 将程序员编写的代码转换为方便进行代码语法分析的树状结构代码。

而我们可以使用 PsiViewer插件 来直观的查看通过 PSI 生成的树状结构,下面两张图可以直观的看出该插件的使用以及树状结构的展示:

PSI示例1.png

PSI示例2.png

实现你的第一个自定义 ktlint 规则

「Talk is cheap. Show me the code」. 因此这里我用一个自定义的 ktlint 规则 -- 不可直接继承 Activity() , 必须继承 BaseActivity 的实现当示例,希望大家能从中了解如何实现自定义规则,示例代码Github.为方便调试,下面示例是在一个可用的 Android 项目下进行,这样方便我们调试,完成开发后,可以迁移到一个独立的 kotlin 项目,方便分发使用,如示例代码Github.

创建自定义 ktlint 规则模块

  • 在项目根文件夹中与app模块处于同一文件夹级别创建一个单独的模块,这里我将模块命名为 custom_rules ;

新建rules模块.png

  • 将新建模块下的 build.gradle 文件修改如下,其中依赖的 kotlin 版本号要与项目根目录的 build.gradle 文件的版本一致:
plugins {
    id 'kotlin'
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.20"
    compileOnly "com.pinterest.ktlint:ktlint-core:0.41.0"
}
  • 告诉 ktlint 查找到我们实现了 RuleSetProvider 的类.在新建模块下的 src->main 下面新建文件夹 resources/META-INF/services,并且在该目录下新建 com.pinterest.ktlint.core.RuleSetProvider 文件,在文件中添加
com.tc.custom_rules.CustomRuleSetProvider

这时候我们的文件目录如图:

自定义ktlint规则step1menu.png

新建规则类实现规则

package com.tc.custom_rules

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType
import com.pinterest.ktlint.core.ast.children
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes

class ExtendBaseRule : Rule("kclass-extend-base-rules") {
    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == KtStubElementTypes.CLASS) {
            println("使用调试打印日志:${node.text}")
            //修饰符为 class 的ASTNode
            var isExtendActivity = false
            //判断该class是否继承了Activity
            for (childNode in node.children()) {
                if (childNode.elementType == KtStubElementTypes.SUPER_TYPE_LIST) {
                    //psi中继承与实现的类
                    for (minChild in childNode.children()) {
                        if (minChild.elementType == KtStubElementTypes.SUPER_TYPE_CALL_ENTRY) {
                            //psi中继承的类,判断继承的ASTNode的文本
                            if (minChild.text == "Activity()") {
                                isExtendActivity = true
                            }
                            break
                        }
                    }
                }
            }
            if (isExtendActivity) {
                //该class是继承了Activity,再判断是不是BaseActivity
                for (childNode in node.children()) {
                    if (childNode.elementType == ElementType.IDENTIFIER) {
                        //第一个标识符,是类名
                        if (isExtendActivity && childNode.text != "BaseActivity") {
                            //该class是继承了Activity,也不是BaseActivity,因此输出错误
                            emit(
                                childNode.startOffset,
                                "Activity请继承BaseActivity!",
                                false
                            )
                            break
                        }
                        break
                    }
                }
            }
        }
    }
}
  • CustomRuleSetProvider 类实现 RuleSetProvider ,返回上面定义的规则
package com.tc.custom_rules

import com.pinterest.ktlint.core.RuleSet
import com.pinterest.ktlint.core.RuleSetProvider

class CustomRuleSetProvider : RuleSetProvider {
    override fun get(): RuleSet = RuleSet(
        "custom-rule-set",
        ExtendBaseRule()
    )
}

与 ktlint 共同使用

  • app 模块的 build.gradle 依赖模块
...
dependencies {
  ...
  ktlint project(':custom_rules')
}

  • 然后终端执行 ./gradlew ktlint ,可以看到我们自定义的规则已经产生作用

执行ktlint日志.png

ktlint报告.png

导出自定义规则的 jar

实际实践中,我们并不可能每次有新项目配置规则的时候都添加一个自定义规则模块,因此我们需要把自定义规则模块导出成 jar ,方便 Android 项目引用。

你可以在刚才的自定义规则模块基础上执行

./gradlew :custom_rules:build

或者把刚才的自定义模块独立成一个 kotlin 项目,执行

./gradlew build

可以在 build->lib 中看到构建出的 jar , 之后就可以发布到 Maven 仓库了。

总结

讲解了如何实现自定义规则,基于 ktlint 和 Gitlab CI/CD 的团队静态代码规范实践这个系列基本上也完结了。
如果我的文章对你有帮助或启发,辛苦大佬们点个赞👍🏻,支持我一下。
如果有错漏,欢迎大佬们指正,也欢迎大家一起讨论,感谢。

参考文档

Gradle 参考文档
Writing your first ktlint rule -- Niklas Baudy
IDEA 程序结构接口 (PSI) 官方参考文档
自定义规则示例代码Github